product name alone made required
This commit is contained in:
@@ -161,3 +161,45 @@ test('a retired drop with nowhere to follow is finished', () => {
|
||||
|
||||
assert.equal(isSettled(retired), true);
|
||||
});
|
||||
|
||||
/* ── Cross-drop contamination ─────────────────────────────────────────────── */
|
||||
|
||||
// An admin can assemble one run from several drops, so a run's manifest can
|
||||
// carry other senders' products. Applying our sheet's price and opening stock
|
||||
// to those would stock someone else's goods into our merchant's branch.
|
||||
test('only our own file contributes products', () => {
|
||||
const run = {
|
||||
...held,
|
||||
status: 'done' as const,
|
||||
files: [
|
||||
{
|
||||
index: 0,
|
||||
filename: 'ours.csv',
|
||||
status: 'done' as const,
|
||||
result: {
|
||||
products: [
|
||||
{ image_id: 'amul_a', brand: 'amul', product_name: 'Ours', disposition: 'inserted' as const },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
filename: 'someone-elses.csv',
|
||||
status: 'done' as const,
|
||||
result: {
|
||||
products: [
|
||||
{ image_id: 'amul_b', brand: 'amul', product_name: 'Theirs', disposition: 'inserted' as const },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies IngestBatch;
|
||||
|
||||
const mine = productsOf(run, ['ours.csv']);
|
||||
assert.equal(mine.length, 1);
|
||||
assert.equal(mine[0]?.product_name, 'Ours');
|
||||
|
||||
// Unfiltered still returns everything — the filter is the caller's decision,
|
||||
// and every caller that prices products must make it.
|
||||
assert.equal(productsOf(run).length, 2);
|
||||
});
|
||||
|
||||
@@ -426,9 +426,29 @@ export async function resolveBatch(batch: IngestBatch, signal?: AbortSignal): Pr
|
||||
}
|
||||
}
|
||||
|
||||
/** Every product a finished batch wrote, across its files. */
|
||||
export function productsOf(batch: IngestBatch): IngestProduct[] {
|
||||
return (batch.files ?? []).flatMap((file) => file.result?.products ?? []);
|
||||
/**
|
||||
* The products a finished run wrote, optionally narrowed to our own files.
|
||||
*
|
||||
* `filenames` is not optional in practice and should always be passed. An admin
|
||||
* can assemble ONE run from several drops — the owning team's own words: "a run
|
||||
* an admin assembled from several drops lists every file in it, so you may see
|
||||
* filenames batched alongside your own" — so a run's manifest can contain other
|
||||
* senders' products.
|
||||
*
|
||||
* Reading all of them was a real hazard, not a tidiness point. The sheet's price
|
||||
* and opening stock are applied to whatever the manifest is matched against, so
|
||||
* a product from someone else's sheet sharing a name with one of our rows would
|
||||
* have been priced and stocked from OUR file, into OUR merchant's branch.
|
||||
*
|
||||
* Filtering by filename is the best this contract allows and it is not airtight:
|
||||
* two senders can both upload `products.csv`. Narrowing by drop would be exact,
|
||||
* and the run's files carry no drop reference to narrow by — worth asking for.
|
||||
*/
|
||||
export function productsOf(batch: IngestBatch, filenames?: readonly string[]): IngestProduct[] {
|
||||
const wanted = filenames ? new Set(filenames) : null;
|
||||
return (batch.files ?? [])
|
||||
.filter((file) => !wanted || wanted.has(file.filename))
|
||||
.flatMap((file) => file.result?.products ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user