feat: resolve POS-receipt productId to webshop productId#17
Merged
Conversation
Receipt items returned by GetReceipt carry a POS-side productId (PosReceiptProduct.id) that is not navigable on ah.nl -- https://www.ah.nl/producten/product/wi767898 returns "product bestaat niet" while the actual webshop id for that line is 171607. PosReceiptProduct.externalIds is declared in the schema but always null in production, so consumers had no way to deep-link or hydrate kassabon lines. Add Client.ConvertPOSIDs which calls the productConvertId(sourceId: Int!) GraphQL field via aliased subfields, resolving an entire receipt in one round-trip. GetReceipt invokes it best-effort and populates the new ReceiptItem.WebshopID; conversion failures log and continue rather than failing the receipt fetch. The CLI receipt show command now prints "wi<id>" alongside each line.
There was a problem hiding this comment.
Pull request overview
Adds POS receipt product-id → AH webshop product-id resolution to the receipts workflow so GetReceipt can hydrate each receipt line with a navigable wi<id> (via GraphQL productConvertId) in a single additional GraphQL round-trip.
Changes:
- Added
ReceiptItem.WebshopIDand clarified thatReceiptItem.ProductIDis the POS-side id. - Implemented
Client.ConvertPOSIDs(aliased GraphQL subfields) and wired it intoClient.GetReceipt(best-effort). - Updated CLI output, docs, and tests (unit + integration) to cover the new behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
types.go |
Adds ReceiptItem.WebshopID and corrects ProductID documentation to reflect POS vs webshop id spaces. |
receipts.go |
Implements POS→webshop id conversion and populates WebshopID during GetReceipt. |
receipts_test.go |
Adds unit tests for conversion batching, dedupe/filtering, and best-effort failure behavior. |
receipt_test.go |
Extends integration test to assert at least one WebshopID resolves and can round-trip via GetProduct. |
doc/albertheijn_api.md |
Documents productConvertId, its sentinel behavior, caveats, and aliased batching pattern. |
cmd/appie/receipt.go |
Shows resolved wi<id> next to each receipt line when available. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| var resp map[string]int | ||
| if err := c.DoGraphQL(ctx, b.String(), nil, &resp); err != nil { | ||
| return nil, fmt.Errorf("convert pos ids: %w", err) |
Comment on lines
+302
to
+303
| // ah.nl), resolved from ProductID via productConvertId. 0 when | ||
| // resolution failed or the API returned its -1 sentinel. |
Comment on lines
+182
to
+183
| // readGraphQLRequest reads and rewinds the body so the handler can both | ||
| // inspect and decode the request. |
Comment on lines
+226
to
+236
| var resp map[string]int | ||
| if err := c.DoGraphQL(ctx, b.String(), nil, &resp); err != nil { | ||
| return nil, fmt.Errorf("convert pos ids: %w", err) | ||
| } | ||
|
|
||
| for i, id := range unique { | ||
| v, ok := resp[fmt.Sprintf("p%d", i)] | ||
| if !ok || v <= 0 { | ||
| continue | ||
| } | ||
| out[id] = v |
- ConvertPOSIDs now returns the (always non-nil) out map alongside errors, so callers that ignore err can safely look up keys without nil-deref. Doc comment updated to reflect the guarantee. - Decode productConvertId aliases through *int instead of int. The GraphQL field is declared `Int` (nullable); a single null alias would previously fail JSON unmarshal and torch the whole batch. Null is now treated identically to the -1 sentinel — covered by the new TestConvertPOSIDsTreatsNullAsUnresolved. - Clarify ReceiptItem.WebshopID doc: the in-memory zero value 0 means "unresolved", and JSON omits 0 entirely (omitempty). - Fix readGraphQLRequest test helper comment — it does not rewind r.Body; document that callers must not read it again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds first-class POS → webshop product-id resolution to the receipts module.
Client.GetReceiptnow populates a newReceiptItem.WebshopIDso consumers can deep-link kassabon lines to ah.nl without an extra round-trip per item.Why
PosReceiptProduct.externalIdsis declared in the GraphQL schema but is alwaysnullin production, soReceiptItem.ProductID(the POS-side integer printed on the kassabon) currently has no way to be turned into a navigable webshop id. The two id spaces are distinct:https://www.ah.nl/producten/product/wi767898returns "Dit product bestaat niet (meer)" while the actual webshop id for that line is171607("Coca-Cola Zero sugar zero caffeïne").The official Appie iOS/Android app shows product cards on every kassabon line, so a resolver clearly exists. Probing the
Querytype surfacedproductConvertId(sourceId: Int!): Int— a single-argument resolver that does exactly this. Verified empirically:The resolver returns
-1when no conversion exists (e.g. when the input is already a webshop id). It does not validate the source id space, so passing arbitrary integers can return real but unrelated webshop ids — only feed it ids that came fromPosReceiptProduct.id. This caveat is captured in theConvertPOSIDsdoc comment and indoc/albertheijn_api.md.What changed
receipts.go— newClient.ConvertPOSIDs(ctx, []int) (map[int]int, error)builds a single GraphQL doc with N aliasedproductConvertIdsubfields (so a whole receipt resolves in one round-trip), dedupes input, drops non-positive ids and the-1sentinel.GetReceiptcalls it best-effort (logs on failure, returns the receipt regardless) and fillsItems[i].WebshopID.GetReceipts(list view) is unchanged — it doesn't return per-product data.types.go— addsReceiptItem.WebshopID int. Updates theProductIDdoc comment to clarify it is the POS-side id, not the webshop id (the previous wording was misleading).cmd/appie/receipt.go—appie receipt showappendswi<id>next to each line when resolution succeeded; blank otherwise.doc/albertheijn_api.md— adds a row to the GraphQL ops table and a#### productConvertIdsubsection covering signature, sentinel, caveat, and the aliased-batch example.TestGetReceiptResolvesWebshopID,TestConvertPOSIDsBatchShapewith dedup verification,TestConvertPOSIDsErrorIsBestEffort) plus an integration assertion inTestGetReceiptIntegrationthat at least one item resolves andGetProduct(WebshopID)round-trips successfully.Heads-up: pre-existing build break under
-tags integrationIndependent of this PR,
maincurrently fails to compile under-tags integration: commit 515edfe addedTestGetShoppingListItemsto bothshoppinglist_test.go(no tag) andappie_integration_test.go(under//go:build integration). To run the integration suite locally I had to rename one of them; the rename is not included in this PR. Happy to send a separate one-line fix if useful.Test plan
just testpasses (3 new unit tests + existing suite).go test -tags integration -run 'TestGetReceiptIntegration' ./...passes against a real account; canonical receipt resolves all 8 lines.appie receipt show <id>annotates every line withwi<id>.