Flow
One run of the widened sync
1Pick stores
Every allowed store credential, repeats removed, sorted. None left means the old default.
2Fetch per store
Each store is read on its own, and the product lookup runs per store only if asked for.
3Build IDs
Store plus sale, discount and line numbers, with a fallback when the store is unknown.
4Merge
Same ID keeps the last copy. The response counts how many were absorbed.
5Cap and save
Merged, stored and cut counts come back. A dry run skips the save.
Changing my credit sync to read every store at once, I found the code that decides whether two sales are the same sale had never looked at the store.
The sync pulls discounts and returns out of the stores' point-of-sale system and turns them into credit events. Before, if you didn't name a store, it used one default credential and saw one view.
Picking the stores
Now, with no store named, it takes every active store credential the caller is allowed to use, removes repeats and sorts them. If there are none, it falls back to the old default. The sort keeps the order the same every run, which matters because the merge later keeps the last copy it sees.
The key that forgot the store
Each event gets an ID so a sale fetched twice by overlapping date windows only counts once. Events go into a map keyed by that ID. The ID was built from the sale number plus the discount and line numbers inside it, with no store anywhere. Each store likely numbers its own sales. So with two stores, two different sales can share a number, and if their discount and line numbers match too, they get the same ID and the second one replaces the first.
Nothing complains when that happens. The total still looks normal, just lower. Counting a sale twice makes a number look too big, and people ask about that. Losing a sale makes it look a bit small, and nobody does.
The same change that added the store loop added the store to the ID:
key = digest(shop.code, sale.ref, line.discount, line.ref)
If the store can't be looked up, a fixed placeholder fills in. A separate fix around then made it try the store it asked for first.
Two stores, one sale number
The test sets up a fake upstream where two stores return the exact same sale. The sync runs with no store named and expects two stores, two events and both credentials used. With the old ID, it could only ever keep one.
That proves the new behavior, not a past loss. Both changes shipped together, so the old ID probably never ran against two stores. I won't claim it lost real rows.
Product details, per store
The product lookup used to run once, before the store loop. In a run with no store named, my reading is that every store would have used the default credential's catalog. It now runs for each store. I'd also made it opt-in earlier that day, because the product list comes back as one huge response I didn't trust to fit in memory, and counting credits doesn't need it.
Counting what the map drops
Dropping repeats is still the job. What I lacked was a count of how many got dropped. The sync response now reports counts at each step: sales fetched, discount rows found, rows skipped, rows with no amount, events made, and events the map absorbed. A test where the same sale shows up in two windows expects one absorbed, which is normal. That count can't tell a real repeat from a collision, so I'd watch for it jumping when a store is added.
The size cap now says what it cut
The stored list has a size limit to protect memory. It used to trim the list and say nothing. Now the response says how many events were merged, stored and cut. I raised the limit too. A dry-run option runs the whole sync and returns the counts without replacing what's stored, so I can try things against real data without overwriting anything. If the store lookup fails, the sync keeps going and counts that instead of stopping.
Two things I haven't tested. My reading is that one store's failed fetch stops the whole run, and that a cut lands on whichever stores come last in the sort.