feat(apicore-sync): field-level ownership between ApiCore and the shop
ApiCore stays the source of truth for beekeeping facts, Wabenhain for everything sales-related; both remain separate packages talking only over ApiCore's HTTP API. - Batches: lot number, fill date, best-before date and filled jar count now follow ApiCore on every run (before: created once, corrections never arrived). remainingGlasses - the shop's own stock - is never written; if it exceeds a corrected fill quantity, the run reports it instead of silently adjusting. - Batches are matched by the new Batch.apicoreBottlingId instead of the lot number, so correcting a lot number in ApiCore no longer creates a second batch. Existing synced batches are adopted once via their lot number. - Harvests: date and year always follow ApiCore, also when published or edited; type, location and text stay as maintained in the admin. - Products stay entirely shop-owned after creation (weight drives the legally required unit price, so it is never changed automatically). - Admin panel: batch update count and stock-conflict notice; intro text states which system owns which fields. Migration is additive (nullable unique column). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
45ca0ea745
commit
d38f4303ff
9 files changed
+272
-42
No files matched your search
@@ -8,6 +8,8 @@ import {
|
||||
deriveJarWeightGrams,
|
||||
slugifyProductName,
|
||||
productSlugCandidates,
|
||||
harvestFactChanges,
|
||||
batchFactChanges,
|
||||
} from "./apicore-mapping";
|
||||
|
||||
function makeProduct(overrides: Partial<ApiCoreProduct> = {}): ApiCoreProduct {
|
||||
@@ -272,3 +274,70 @@ describe("isSellableStatus", () => {
|
||||
expect(isSellableStatus("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("harvestFactChanges", () => {
|
||||
test("liefert nichts, wenn Datum und Jahr unveraendert sind", () => {
|
||||
// Arrange
|
||||
const date = new Date("2026-07-14T00:00:00.000Z");
|
||||
|
||||
// Act
|
||||
const changes = harvestFactChanges({ harvestDate: date, year: 2026 }, new Date(date));
|
||||
|
||||
// Assert
|
||||
expect(changes).toEqual({});
|
||||
});
|
||||
|
||||
test("uebernimmt ein in ApiCore korrigiertes Erntedatum samt Jahr", () => {
|
||||
// Arrange
|
||||
const existing = { harvestDate: new Date("2025-12-31T12:00:00.000Z"), year: 2025 };
|
||||
const corrected = new Date("2026-01-02T12:00:00.000Z");
|
||||
|
||||
// Act
|
||||
const changes = harvestFactChanges(existing, corrected);
|
||||
|
||||
// Assert
|
||||
expect(changes).toEqual({ harvestDate: corrected, year: 2026 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("batchFactChanges", () => {
|
||||
const base = {
|
||||
code: "WH-2026-0001",
|
||||
fillDate: new Date("2026-08-01T00:00:00.000Z"),
|
||||
bestBeforeDate: new Date("2028-08-01T00:00:00.000Z"),
|
||||
glassCount: 100,
|
||||
};
|
||||
|
||||
test("aktualisiert nur die ApiCore-eigenen Felder, die sich geaendert haben", () => {
|
||||
// Arrange: MHD in ApiCore korrigiert, Shop hat 40 Glaeser verkauft.
|
||||
const source = { ...base, bestBeforeDate: new Date("2028-07-31T00:00:00.000Z") };
|
||||
|
||||
// Act
|
||||
const result = batchFactChanges({ ...base, remainingGlasses: 60 }, source);
|
||||
|
||||
// Assert
|
||||
expect(result.changes).toEqual({ bestBeforeDate: source.bestBeforeDate });
|
||||
expect(result.changes).not.toHaveProperty("remainingGlasses");
|
||||
expect(result.stockExceedsFilled).toBe(false);
|
||||
});
|
||||
|
||||
test("uebernimmt eine korrigierte Losnummer statt eine zweite Charge anzulegen", () => {
|
||||
// Act
|
||||
const result = batchFactChanges({ ...base, remainingGlasses: 100 }, { ...base, code: "WH-2026-0002" });
|
||||
|
||||
// Assert
|
||||
expect(result.changes).toEqual({ code: "WH-2026-0002" });
|
||||
});
|
||||
|
||||
test("meldet einen Shop-Bestand ueber der korrigierten Abfuellmenge, statt ihn anzugleichen", () => {
|
||||
// Arrange: ApiCore korrigiert 100 auf 80 Glaeser, im Shop stehen noch 90.
|
||||
const source = { ...base, glassCount: 80 };
|
||||
|
||||
// Act
|
||||
const result = batchFactChanges({ ...base, remainingGlasses: 90 }, source);
|
||||
|
||||
// Assert
|
||||
expect(result.changes).toEqual({ glassCount: 80 });
|
||||
expect(result.stockExceedsFilled).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user