@saasicat/persistence-testing 1.0.0-rc.14 → 1.0.0-rc.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,6 +2,100 @@
2
2
  import assert from "node:assert/strict";
3
3
  import { after, before, beforeEach, describe, test } from "node:test";
4
4
  var LOCK_HOLD_MS = 150;
5
+ var OFFER = {
6
+ planKey: "STANDARD",
7
+ planVersionId: null,
8
+ billingCycle: "monthly",
9
+ priceBreakdown: {
10
+ currency: "EUR",
11
+ billingCycle: "monthly",
12
+ planNet: 49,
13
+ bundlesNet: 0,
14
+ regularNet: 49,
15
+ effectiveNet: 49,
16
+ vatRate: 19,
17
+ effectiveGross: 58.31
18
+ }
19
+ };
20
+ function partiesWith(subscriberId, legalName) {
21
+ return {
22
+ subscriberId,
23
+ subscriber: {
24
+ customerNumber: "K-10001",
25
+ legalName,
26
+ vatId: "DE123456789",
27
+ taxNumber: null,
28
+ addressLine1: "Hauptstra\xDFe 1",
29
+ addressLine2: null,
30
+ postalCode: "10115",
31
+ city: "Berlin",
32
+ country: "DE"
33
+ },
34
+ issuer: {
35
+ legalName: "Example Software GmbH",
36
+ vatId: "DE987654321",
37
+ taxNumber: "12/345/67890",
38
+ addressLine1: "Werkstra\xDFe 5",
39
+ addressLine2: null,
40
+ postalCode: "80331",
41
+ city: "M\xFCnchen",
42
+ country: "DE"
43
+ }
44
+ };
45
+ }
46
+ function subscriberFor(tenantId, legalName) {
47
+ return {
48
+ tenantId,
49
+ legalName,
50
+ vatId: null,
51
+ taxNumber: null,
52
+ addressLine1: null,
53
+ addressLine2: null,
54
+ postalCode: null,
55
+ city: null,
56
+ country: null,
57
+ invoiceEmail: null,
58
+ customerNumberPrefix: ""
59
+ };
60
+ }
61
+ function contractFromOffer(offerId, parties) {
62
+ return {
63
+ tenantId: `tenant-${offerId}`,
64
+ parties,
65
+ effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
66
+ originalOfferId: offerId,
67
+ priceSnapshot: {
68
+ currency: "EUR",
69
+ billingCycle: "monthly",
70
+ subtotalNet: 49,
71
+ discountNet: 0,
72
+ totalNet: 49,
73
+ vatRate: 19,
74
+ totalGross: 58.31
75
+ },
76
+ lineItems: [
77
+ {
78
+ kind: "plan",
79
+ sourceKey: "STANDARD",
80
+ sourceVersionId: null,
81
+ titleSnapshot: "Standard",
82
+ descriptionSnapshot: null,
83
+ quantity: 1,
84
+ unit: null,
85
+ priceNet: 49,
86
+ priceGross: 58.31,
87
+ billingCycle: "monthly",
88
+ currency: "EUR",
89
+ taxRate: 19,
90
+ taxAmount: 9.31,
91
+ minimumTermUntil: null,
92
+ featuresSnapshot: [],
93
+ quotaEffectsSnapshot: {},
94
+ metadata: null
95
+ }
96
+ ]
97
+ };
98
+ }
5
99
  var CONTRACT_GAPS = {
6
100
  atomicPlanBinding: {
7
101
  reason: "adapter does not expose atomic plan-binding writes",
@@ -99,8 +193,16 @@ var CONTRACT_GAPS = {
99
193
  present: ({ adapter }) => Boolean(adapter.mfa)
100
194
  },
101
195
  subscriptionContracts: {
102
- reason: "adapter provides no SubscriptionContractRepository",
103
- present: ({ adapter }) => Boolean(adapter.subscriptionContractRepository)
196
+ reason: "adapter provides no SubscriptionContractRepository, or no subscriber seed for it",
197
+ present: ({ adapter, seed }) => Boolean(adapter.subscriptionContractRepository && seed.createSubscriber)
198
+ },
199
+ subscribers: {
200
+ reason: "adapter provides no SubscriberRepository",
201
+ present: ({ adapter }) => Boolean(adapter.subscriberRepository)
202
+ },
203
+ checkoutOffers: {
204
+ reason: "adapter provides no CheckoutOfferRepository",
205
+ present: ({ adapter }) => Boolean(adapter.checkoutOfferRepository)
104
206
  },
105
207
  appliedSettings: {
106
208
  reason: "adapter provides no AppliedSettingsPort",
@@ -1291,14 +1393,18 @@ function persistenceAdapterContract(options) {
1291
1393
  });
1292
1394
  test("a contract keeps what was agreed, and ending it does not rewrite it", async (t) => {
1293
1395
  const contracts = harness.adapter.subscriptionContractRepository;
1294
- if (!contracts) {
1396
+ const createSubscriber = harness.seed.createSubscriber;
1397
+ if (!contracts || !createSubscriber) {
1295
1398
  missing(t, "subscriptionContracts");
1296
1399
  return;
1297
1400
  }
1298
1401
  const tenantId = "tenant-contract-lifecycle";
1299
1402
  const signedAt = /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z");
1403
+ const { subscriberId } = await createSubscriber({ legalName: "Meier GmbH" });
1404
+ const parties = partiesWith(subscriberId, "Meier GmbH");
1300
1405
  const created = await contracts.create({
1301
1406
  tenantId,
1407
+ parties,
1302
1408
  effectiveFrom: signedAt,
1303
1409
  priceSnapshot: {
1304
1410
  currency: "EUR",
@@ -1359,6 +1465,10 @@ function persistenceAdapterContract(options) {
1359
1465
  });
1360
1466
  assert.equal(created.status, "active");
1361
1467
  assert.equal(created.tenantId, tenantId);
1468
+ assert.equal(created.subscriberId, subscriberId);
1469
+ assert.deepEqual(created.subscriber, parties.subscriber);
1470
+ assert.deepEqual(created.issuer, parties.issuer);
1471
+ assert.equal(created.partiesMigrated, false);
1362
1472
  assert.equal(created.lineItems.length, 2);
1363
1473
  assert.deepEqual(created.originalBundleVersionIds, ["bundle-version-1"]);
1364
1474
  assert.deepEqual(created.termsSnapshot, { noticePeriodDays: 30 });
@@ -1391,6 +1501,8 @@ function persistenceAdapterContract(options) {
1391
1501
  assert.equal(bundleLine.metadata, null);
1392
1502
  const readBack = await contracts.findById(created.id);
1393
1503
  assert.ok(readBack, "contract expected by id");
1504
+ assert.deepEqual(readBack.subscriber, parties.subscriber, "the copy is stored");
1505
+ assert.deepEqual(readBack.issuer, parties.issuer);
1394
1506
  assert.equal(
1395
1507
  readBack.lineItems.length,
1396
1508
  2,
@@ -1447,11 +1559,14 @@ function persistenceAdapterContract(options) {
1447
1559
  });
1448
1560
  test("a successor takes over without erasing the contract it replaces", async (t) => {
1449
1561
  const contracts = harness.adapter.subscriptionContractRepository;
1450
- if (!contracts) {
1562
+ const createSubscriber = harness.seed.createSubscriber;
1563
+ if (!contracts || !createSubscriber) {
1451
1564
  missing(t, "subscriptionContracts");
1452
1565
  return;
1453
1566
  }
1454
1567
  const tenantId = "tenant-contract-succession";
1568
+ const { subscriberId } = await createSubscriber({ legalName: "Nachfolger KG" });
1569
+ const parties = partiesWith(subscriberId, "Nachfolger KG");
1455
1570
  const handover = /* @__PURE__ */ new Date("2026-04-01T00:00:00.000Z");
1456
1571
  const lineAt = (priceNet, priceGross) => ({
1457
1572
  kind: "plan",
@@ -1483,6 +1598,7 @@ function persistenceAdapterContract(options) {
1483
1598
  });
1484
1599
  const first = await contracts.create({
1485
1600
  tenantId,
1601
+ parties,
1486
1602
  effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
1487
1603
  priceSnapshot: priceAt(19.9, 23.68),
1488
1604
  lineItems: [lineAt(19.9, 23.68)]
@@ -1500,6 +1616,7 @@ function persistenceAdapterContract(options) {
1500
1616
  });
1501
1617
  const second = await contracts.create({
1502
1618
  tenantId,
1619
+ parties,
1503
1620
  effectiveFrom: handover,
1504
1621
  priceSnapshot: priceAt(24.9, 29.63),
1505
1622
  lineItems: [lineAt(24.9, 29.63)]
@@ -1549,13 +1666,17 @@ function persistenceAdapterContract(options) {
1549
1666
  });
1550
1667
  test("a line keeps the currency and the tax it was booked with", async (t) => {
1551
1668
  const contracts = harness.adapter.subscriptionContractRepository;
1552
- if (!contracts) {
1669
+ const createSubscriber = harness.seed.createSubscriber;
1670
+ if (!contracts || !createSubscriber) {
1553
1671
  missing(t, "subscriptionContracts");
1554
1672
  return;
1555
1673
  }
1556
1674
  const tenantId = "tenant-contract-money-facts";
1675
+ const { subscriberId } = await createSubscriber({ legalName: "Z\xFCrich AG" });
1676
+ const parties = { ...partiesWith(subscriberId, "Z\xFCrich AG"), issuer: null };
1557
1677
  const created = await contracts.create({
1558
1678
  tenantId,
1679
+ parties,
1559
1680
  effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
1560
1681
  priceSnapshot: {
1561
1682
  currency: "CHF",
@@ -1629,6 +1750,414 @@ function persistenceAdapterContract(options) {
1629
1750
  );
1630
1751
  }
1631
1752
  });
1753
+ test("a contract written on a transaction is undone with it, and found by its offer", async (t) => {
1754
+ const { adapter, seed } = harness;
1755
+ const contracts = adapter.subscriptionContractRepository;
1756
+ if (!contracts || !seed.createSubscriber) {
1757
+ missing(t, "subscriptionContracts");
1758
+ return;
1759
+ }
1760
+ if (!adapter.capabilities.transactions) {
1761
+ t.skip("adapter declares no transaction capability");
1762
+ return;
1763
+ }
1764
+ const { subscriberId } = await seed.createSubscriber({ legalName: "Angebot GmbH" });
1765
+ const parties = partiesWith(subscriberId, "Angebot GmbH");
1766
+ await assert.rejects(
1767
+ adapter.transactionRunner.run(async (tx) => {
1768
+ await contracts.create(contractFromOffer("offer-rolled-back", parties), tx);
1769
+ throw new Error("the conclusion fails after the contract is written");
1770
+ })
1771
+ );
1772
+ assert.equal(
1773
+ await contracts.findByOriginalOfferId("offer-rolled-back"),
1774
+ null,
1775
+ "the contract outlived its transaction"
1776
+ );
1777
+ const kept = await adapter.transactionRunner.run(
1778
+ (tx) => contracts.create(contractFromOffer("offer-kept", parties), tx)
1779
+ );
1780
+ const found = await contracts.findByOriginalOfferId("offer-kept");
1781
+ assert.equal(found?.id, kept.id);
1782
+ assert.equal(found?.lineItems.length, 1, "with its lines");
1783
+ assert.equal(await contracts.findByOriginalOfferId("offer-nobody-concluded"), null);
1784
+ });
1785
+ test("a subscriber is created live for its tenant, and numbered by the database", async (t) => {
1786
+ const subscribers = harness.adapter.subscriberRepository;
1787
+ if (!subscribers) {
1788
+ missing(t, "subscribers");
1789
+ return;
1790
+ }
1791
+ const first = await subscribers.createForTenant({
1792
+ ...subscriberFor("tenant-subscriber-first", "Erste Autohaus GmbH"),
1793
+ vatId: "DE123456789",
1794
+ addressLine1: "Hauptstra\xDFe 1",
1795
+ postalCode: "10115",
1796
+ city: "Berlin",
1797
+ country: "DE",
1798
+ invoiceEmail: "rechnung@erste.example",
1799
+ customerNumberPrefix: "K-"
1800
+ });
1801
+ const second = await subscribers.createForTenant(
1802
+ subscriberFor("tenant-subscriber-second", "Zweiter Verein e.V.")
1803
+ );
1804
+ assert.ok(first && second, "both tenants had no subscriber");
1805
+ const numberOf = (customerNumber) => Number(customerNumber.replace("K-", ""));
1806
+ assert.ok(first.customerNumber.startsWith("K-"), first.customerNumber);
1807
+ assert.equal(numberOf(second.customerNumber), numberOf(first.customerNumber) + 1);
1808
+ assert.ok(numberOf(first.customerNumber) >= 10001, first.customerNumber);
1809
+ const found = await subscribers.findByTenantId("tenant-subscriber-first");
1810
+ assert.deepEqual(
1811
+ found && {
1812
+ id: found.id,
1813
+ customerNumber: found.customerNumber,
1814
+ tenantId: found.tenantId,
1815
+ legalName: found.legalName,
1816
+ vatId: found.vatId,
1817
+ taxNumber: found.taxNumber,
1818
+ addressLine1: found.addressLine1,
1819
+ addressLine2: found.addressLine2,
1820
+ postalCode: found.postalCode,
1821
+ city: found.city,
1822
+ country: found.country,
1823
+ invoiceEmail: found.invoiceEmail,
1824
+ migrated: found.migrated
1825
+ },
1826
+ {
1827
+ id: first.id,
1828
+ customerNumber: first.customerNumber,
1829
+ tenantId: "tenant-subscriber-first",
1830
+ legalName: "Erste Autohaus GmbH",
1831
+ vatId: "DE123456789",
1832
+ // The nullable half, so an adapter writing '' is caught too.
1833
+ taxNumber: null,
1834
+ addressLine1: "Hauptstra\xDFe 1",
1835
+ addressLine2: null,
1836
+ postalCode: "10115",
1837
+ city: "Berlin",
1838
+ country: "DE",
1839
+ invoiceEmail: "rechnung@erste.example",
1840
+ migrated: false
1841
+ }
1842
+ );
1843
+ assert.equal(
1844
+ (await subscribers.findById(second.id))?.tenantId,
1845
+ "tenant-subscriber-second"
1846
+ );
1847
+ assert.equal(await subscribers.findByTenantId("tenant-without-subscriber"), null);
1848
+ assert.equal(await subscribers.findById("subscriber-nobody-created"), null);
1849
+ });
1850
+ test("a tenant has one live subscriber: a second is refused, and the transaction survives it", async (t) => {
1851
+ const { adapter } = harness;
1852
+ const subscribers = adapter.subscriberRepository;
1853
+ if (!subscribers) {
1854
+ missing(t, "subscribers");
1855
+ return;
1856
+ }
1857
+ if (!adapter.capabilities.transactions) {
1858
+ t.skip("adapter declares no transaction capability");
1859
+ return;
1860
+ }
1861
+ const first = await subscribers.createForTenant(
1862
+ subscriberFor("tenant-one-subscriber", "Einzig GmbH")
1863
+ );
1864
+ const afterRefusal = await adapter.transactionRunner.run(async (tx) => {
1865
+ const refused = await subscribers.createForTenant(
1866
+ subscriberFor("tenant-one-subscriber", "Doppelt GmbH"),
1867
+ tx
1868
+ );
1869
+ assert.equal(refused, null, "a second live subscriber was created");
1870
+ return subscribers.createForTenant(
1871
+ subscriberFor("tenant-after-refusal", "Danach GmbH"),
1872
+ tx
1873
+ );
1874
+ });
1875
+ assert.equal(
1876
+ (await subscribers.findByTenantId("tenant-one-subscriber"))?.id,
1877
+ first?.id
1878
+ );
1879
+ assert.equal(
1880
+ (await subscribers.findByTenantId("tenant-after-refusal"))?.id,
1881
+ afterRefusal?.id,
1882
+ "the transaction did not survive the refusal"
1883
+ );
1884
+ });
1885
+ test("creating a subscriber for one tenant twice at once ends with one", async (t) => {
1886
+ const subscribers = harness.adapter.subscriberRepository;
1887
+ if (!subscribers) {
1888
+ missing(t, "subscribers");
1889
+ return;
1890
+ }
1891
+ const results = await Promise.all([
1892
+ subscribers.createForTenant(subscriberFor("tenant-at-once", "Gleichzeitig A")),
1893
+ subscribers.createForTenant(subscriberFor("tenant-at-once", "Gleichzeitig B"))
1894
+ ]);
1895
+ const created = results.filter((result) => result !== null);
1896
+ assert.equal(created.length, 1, `${created.length} subscribers were created`);
1897
+ assert.equal((await subscribers.findByTenantId("tenant-at-once"))?.id, created[0].id);
1898
+ });
1899
+ test("a subscriber written on a transaction is undone with it", async (t) => {
1900
+ const { adapter } = harness;
1901
+ const subscribers = adapter.subscriberRepository;
1902
+ if (!subscribers) {
1903
+ missing(t, "subscribers");
1904
+ return;
1905
+ }
1906
+ if (!adapter.capabilities.transactions) {
1907
+ t.skip("adapter declares no transaction capability");
1908
+ return;
1909
+ }
1910
+ await assert.rejects(
1911
+ adapter.transactionRunner.run(async (tx) => {
1912
+ await subscribers.createForTenant(
1913
+ subscriberFor("tenant-rolled-back", "Zur\xFCckgerollt GmbH"),
1914
+ tx
1915
+ );
1916
+ throw new Error("the tenant is not created after all");
1917
+ })
1918
+ );
1919
+ assert.equal(await subscribers.findByTenantId("tenant-rolled-back"), null);
1920
+ assert.ok(
1921
+ await subscribers.createForTenant(
1922
+ subscriberFor("tenant-rolled-back", "Zur\xFCckgerollt GmbH")
1923
+ )
1924
+ );
1925
+ });
1926
+ test("a contact change writes what it names and keeps the rest", async (t) => {
1927
+ const subscribers = harness.adapter.subscriberRepository;
1928
+ if (!subscribers) {
1929
+ missing(t, "subscribers");
1930
+ return;
1931
+ }
1932
+ const created = await subscribers.createForTenant({
1933
+ ...subscriberFor("tenant-contact", "Kontakt GmbH"),
1934
+ addressLine2: "Hinterhaus",
1935
+ city: "Hamburg"
1936
+ });
1937
+ assert.ok(created);
1938
+ const changed = await subscribers.updateContact(created.id, {
1939
+ city: "Bremen",
1940
+ addressLine2: null,
1941
+ invoiceEmail: "buchhaltung@kontakt.example"
1942
+ });
1943
+ assert.deepEqual(
1944
+ changed && [
1945
+ changed.city,
1946
+ changed.addressLine2,
1947
+ changed.invoiceEmail,
1948
+ changed.legalName,
1949
+ changed.tenantId
1950
+ ],
1951
+ ["Bremen", null, "buchhaltung@kontakt.example", "Kontakt GmbH", "tenant-contact"]
1952
+ );
1953
+ assert.equal((await subscribers.findById(created.id))?.city, "Bremen");
1954
+ assert.equal(
1955
+ await subscribers.updateContact("subscriber-nobody-created", { city: "Kiel" }),
1956
+ null
1957
+ );
1958
+ });
1959
+ test("a correction records the values it replaced, and nothing when nothing moves", async (t) => {
1960
+ const subscribers = harness.adapter.subscriberRepository;
1961
+ if (!subscribers) {
1962
+ missing(t, "subscribers");
1963
+ return;
1964
+ }
1965
+ const created = await subscribers.createForTenant(
1966
+ subscriberFor("tenant-correction", "Mueller GmbH")
1967
+ );
1968
+ assert.ok(created);
1969
+ const at = (day) => new Date(Date.UTC(2026, 8, day));
1970
+ const first = await subscribers.correctIdentity(created.id, {
1971
+ corrected: { legalName: "M\xFCller GmbH", vatId: "DE123456789", taxNumber: null },
1972
+ reason: "Umlaut lost when the registration was typed",
1973
+ correctedBy: "operator:anna",
1974
+ correctedAt: at(1)
1975
+ });
1976
+ assert.deepEqual(
1977
+ first?.correction && {
1978
+ previous: first.correction.previous,
1979
+ corrected: first.correction.corrected,
1980
+ reason: first.correction.reason,
1981
+ correctedBy: first.correction.correctedBy,
1982
+ correctedAt: first.correction.correctedAt.getTime()
1983
+ },
1984
+ {
1985
+ previous: { legalName: "Mueller GmbH", vatId: null },
1986
+ corrected: { legalName: "M\xFCller GmbH", vatId: "DE123456789" },
1987
+ reason: "Umlaut lost when the registration was typed",
1988
+ correctedBy: "operator:anna",
1989
+ correctedAt: at(1).getTime()
1990
+ }
1991
+ );
1992
+ assert.equal(first?.subscriber.legalName, "M\xFCller GmbH");
1993
+ assert.equal((await subscribers.findById(created.id))?.vatId, "DE123456789");
1994
+ const unchanged = await subscribers.correctIdentity(created.id, {
1995
+ corrected: { legalName: "M\xFCller GmbH" },
1996
+ reason: "Clicked twice",
1997
+ correctedBy: "operator:anna",
1998
+ correctedAt: at(2)
1999
+ });
2000
+ assert.equal(
2001
+ unchanged?.correction,
2002
+ null,
2003
+ "a correction that moved nothing was recorded"
2004
+ );
2005
+ await subscribers.correctIdentity(created.id, {
2006
+ corrected: { vatId: "DE999999999" },
2007
+ reason: "Wrong VAT id on the first correction",
2008
+ correctedBy: "operator:ben",
2009
+ correctedAt: at(3)
2010
+ });
2011
+ const listed = await subscribers.listCorrections(created.id);
2012
+ assert.deepEqual(
2013
+ listed.map((correction) => correction.reason),
2014
+ [
2015
+ "Wrong VAT id on the first correction",
2016
+ "Umlaut lost when the registration was typed"
2017
+ ],
2018
+ "corrections come back the latest first, and only the two that moved something"
2019
+ );
2020
+ assert.equal(
2021
+ await subscribers.correctIdentity("subscriber-nobody-created", {
2022
+ corrected: { legalName: "Niemand" },
2023
+ reason: "none",
2024
+ correctedBy: "operator:anna",
2025
+ correctedAt: at(4)
2026
+ }),
2027
+ null
2028
+ );
2029
+ });
2030
+ test("two corrections at once each record the value the other left behind", async (t) => {
2031
+ const { adapter } = harness;
2032
+ const subscribers = adapter.subscriberRepository;
2033
+ if (!subscribers) {
2034
+ missing(t, "subscribers");
2035
+ return;
2036
+ }
2037
+ if (!adapter.capabilities.transactions || !adapter.capabilities.pessimisticLocking) {
2038
+ t.skip("adapter declares no transactions or no row locks");
2039
+ return;
2040
+ }
2041
+ const created = await subscribers.createForTenant(
2042
+ subscriberFor("tenant-corrected-twice", "Original GmbH")
2043
+ );
2044
+ assert.ok(created);
2045
+ const correctTo = (legalName) => adapter.transactionRunner.run(async (tx) => {
2046
+ await subscribers.correctIdentity(
2047
+ created.id,
2048
+ {
2049
+ corrected: { legalName },
2050
+ reason: `to ${legalName}`,
2051
+ correctedBy: "operator:anna",
2052
+ correctedAt: /* @__PURE__ */ new Date()
2053
+ },
2054
+ tx
2055
+ );
2056
+ await sleep(LOCK_HOLD_MS);
2057
+ });
2058
+ await Promise.all([
2059
+ correctTo("Erste Korrektur GmbH"),
2060
+ correctTo("Zweite Korrektur GmbH")
2061
+ ]);
2062
+ const listed = await subscribers.listCorrections(created.id);
2063
+ assert.equal(listed.length, 2);
2064
+ const replaced = listed.map((correction) => correction.previous.legalName).sort();
2065
+ const written = listed.map((correction) => correction.corrected.legalName);
2066
+ const current = (await subscribers.findById(created.id))?.legalName;
2067
+ assert.ok(replaced.includes("Original GmbH"), JSON.stringify(listed));
2068
+ const secondReplaced = replaced.find((name) => name !== "Original GmbH");
2069
+ assert.ok(
2070
+ secondReplaced !== void 0 && written.includes(secondReplaced),
2071
+ `a correction recorded a value it did not replace: ${JSON.stringify(listed)}`
2072
+ );
2073
+ assert.ok(current !== void 0 && written.includes(current));
2074
+ assert.notEqual(secondReplaced, current);
2075
+ });
2076
+ test("a contract keeps its subscriber's copy after the subscriber is corrected", async (t) => {
2077
+ const { adapter } = harness;
2078
+ const subscribers = adapter.subscriberRepository;
2079
+ const contracts = adapter.subscriptionContractRepository;
2080
+ if (!subscribers) {
2081
+ missing(t, "subscribers");
2082
+ return;
2083
+ }
2084
+ if (!contracts || !harness.seed.createSubscriber) {
2085
+ missing(t, "subscriptionContracts");
2086
+ return;
2087
+ }
2088
+ const subscriber = await subscribers.createForTenant(
2089
+ subscriberFor("tenant-copy-kept", "Vorher GmbH")
2090
+ );
2091
+ assert.ok(subscriber);
2092
+ const parties = partiesWith(subscriber.id, "Vorher GmbH");
2093
+ const contract = await contracts.create({
2094
+ ...contractFromOffer("offer-copy-kept", parties),
2095
+ tenantId: "tenant-copy-kept"
2096
+ });
2097
+ await subscribers.correctIdentity(subscriber.id, {
2098
+ corrected: { legalName: "Nachher GmbH" },
2099
+ reason: "Change of name of the same company",
2100
+ correctedBy: "operator:anna",
2101
+ correctedAt: /* @__PURE__ */ new Date()
2102
+ });
2103
+ const readBack = await contracts.findById(contract.id);
2104
+ assert.equal(readBack?.subscriberId, subscriber.id);
2105
+ assert.equal(
2106
+ readBack?.subscriber.legalName,
2107
+ "Vorher GmbH",
2108
+ "the contract followed a correction of the live record"
2109
+ );
2110
+ });
2111
+ test("an offer is consumed once, whoever asks first", async (t) => {
2112
+ const offers = harness.adapter.checkoutOfferRepository;
2113
+ if (!offers) {
2114
+ missing(t, "checkoutOffers");
2115
+ return;
2116
+ }
2117
+ const offer = await offers.create(OFFER);
2118
+ assert.equal(offer.status, "open");
2119
+ const attempts = await Promise.allSettled([
2120
+ offers.consume(offer.id),
2121
+ offers.consume(offer.id)
2122
+ ]);
2123
+ assert.equal(
2124
+ attempts.filter((attempt) => attempt.status === "fulfilled").length,
2125
+ 1,
2126
+ "exactly one consume wins"
2127
+ );
2128
+ const consumed = await offers.findById(offer.id);
2129
+ assert.equal(consumed?.status, "consumed");
2130
+ assert.ok(consumed?.consumedAt, "and it records when");
2131
+ await assert.rejects(() => offers.consume(offer.id), "a later consume is refused");
2132
+ assert.equal(
2133
+ (await offers.findById(offer.id))?.consumedAt,
2134
+ consumed.consumedAt,
2135
+ "and changes nothing"
2136
+ );
2137
+ });
2138
+ test("a consume on a transaction that rolls back leaves the offer open", async (t) => {
2139
+ const { adapter } = harness;
2140
+ const offers = adapter.checkoutOfferRepository;
2141
+ if (!offers) {
2142
+ missing(t, "checkoutOffers");
2143
+ return;
2144
+ }
2145
+ if (!adapter.capabilities.transactions) {
2146
+ t.skip("adapter declares no transaction capability");
2147
+ return;
2148
+ }
2149
+ const offer = await offers.create(OFFER);
2150
+ await assert.rejects(
2151
+ adapter.transactionRunner.run(async (tx) => {
2152
+ await offers.consume(offer.id, tx);
2153
+ throw new Error("the contract after the consume fails");
2154
+ })
2155
+ );
2156
+ const afterwards = await offers.findById(offer.id);
2157
+ assert.equal(afterwards?.status, "open", "the consume outlived its transaction");
2158
+ assert.equal(afterwards?.consumedAt, null);
2159
+ assert.equal((await offers.consume(offer.id)).status, "consumed");
2160
+ });
1632
2161
  const SETTINGS = {
1633
2162
  app: { name: "Demo" },
1634
2163
  currency: "EUR",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/persistence-testing",
3
- "version": "1.0.0-rc.14",
3
+ "version": "1.0.0-rc.16",
4
4
  "description": "Contract test kit for SaaSiCat persistence adapters: one node:test suite that every adapter (Prisma, Drizzle, ...) must pass against a real database — locks, transaction rollback, atomic promo claims, tenant isolation, audit/MFA roundtrips.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -23,7 +23,7 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@saasicat/core": "^1.0.0-rc.14"
26
+ "@saasicat/core": "^1.0.0-rc.16"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^25.6.0",