@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/README.md CHANGED
@@ -16,6 +16,9 @@ Verified scenarios:
16
16
  - PlanVersion and BundleVersion validity windows with auto-succession
17
17
  - `countByPlanVersionId` counts current AND pending bindings in one query
18
18
  - transaction rollback discards writes
19
+ - a contract written on a transaction is undone with it, and found by the offer it came from
20
+ - a checkout offer is consumed once, whoever asks first, and a consume on a rolled-back transaction
21
+ leaves it open
19
22
  - `findByTenantIdLocked` serializes concurrent transactions (row lock)
20
23
  - concurrent `claimSlot` grants exactly `maxRedemptions` slots
21
24
  - claim / exhaust / release lifecycle
@@ -91,7 +94,7 @@ persistenceAdapterContract({
91
94
  }),
92
95
  // The parts this adapter deliberately does not provide. A part named here
93
96
  // that the harness does provide fails the suite as well.
94
- gaps: ['subscriptionContracts', 'appliedSettings'],
97
+ gaps: ['subscriptionContracts', 'subscribers', 'checkoutOffers', 'appliedSettings'],
95
98
  });
96
99
  ```
97
100
 
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- fe1ab043363d0764aaf580f2167335fcb0c2659c445bcc9540708782cb751a27
1
+ 1ca79840c535585e92be3594dc7e1b240dfd47e78e98d7577fe19a1cd7e51faf
package/dist/index.cjs CHANGED
@@ -38,6 +38,100 @@ module.exports = __toCommonJS(index_exports);
38
38
  var import_strict = __toESM(require("node:assert/strict"), 1);
39
39
  var import_node_test = require("node:test");
40
40
  var LOCK_HOLD_MS = 150;
41
+ var OFFER = {
42
+ planKey: "STANDARD",
43
+ planVersionId: null,
44
+ billingCycle: "monthly",
45
+ priceBreakdown: {
46
+ currency: "EUR",
47
+ billingCycle: "monthly",
48
+ planNet: 49,
49
+ bundlesNet: 0,
50
+ regularNet: 49,
51
+ effectiveNet: 49,
52
+ vatRate: 19,
53
+ effectiveGross: 58.31
54
+ }
55
+ };
56
+ function partiesWith(subscriberId, legalName) {
57
+ return {
58
+ subscriberId,
59
+ subscriber: {
60
+ customerNumber: "K-10001",
61
+ legalName,
62
+ vatId: "DE123456789",
63
+ taxNumber: null,
64
+ addressLine1: "Hauptstra\xDFe 1",
65
+ addressLine2: null,
66
+ postalCode: "10115",
67
+ city: "Berlin",
68
+ country: "DE"
69
+ },
70
+ issuer: {
71
+ legalName: "Example Software GmbH",
72
+ vatId: "DE987654321",
73
+ taxNumber: "12/345/67890",
74
+ addressLine1: "Werkstra\xDFe 5",
75
+ addressLine2: null,
76
+ postalCode: "80331",
77
+ city: "M\xFCnchen",
78
+ country: "DE"
79
+ }
80
+ };
81
+ }
82
+ function subscriberFor(tenantId, legalName) {
83
+ return {
84
+ tenantId,
85
+ legalName,
86
+ vatId: null,
87
+ taxNumber: null,
88
+ addressLine1: null,
89
+ addressLine2: null,
90
+ postalCode: null,
91
+ city: null,
92
+ country: null,
93
+ invoiceEmail: null,
94
+ customerNumberPrefix: ""
95
+ };
96
+ }
97
+ function contractFromOffer(offerId, parties) {
98
+ return {
99
+ tenantId: `tenant-${offerId}`,
100
+ parties,
101
+ effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
102
+ originalOfferId: offerId,
103
+ priceSnapshot: {
104
+ currency: "EUR",
105
+ billingCycle: "monthly",
106
+ subtotalNet: 49,
107
+ discountNet: 0,
108
+ totalNet: 49,
109
+ vatRate: 19,
110
+ totalGross: 58.31
111
+ },
112
+ lineItems: [
113
+ {
114
+ kind: "plan",
115
+ sourceKey: "STANDARD",
116
+ sourceVersionId: null,
117
+ titleSnapshot: "Standard",
118
+ descriptionSnapshot: null,
119
+ quantity: 1,
120
+ unit: null,
121
+ priceNet: 49,
122
+ priceGross: 58.31,
123
+ billingCycle: "monthly",
124
+ currency: "EUR",
125
+ taxRate: 19,
126
+ taxAmount: 9.31,
127
+ minimumTermUntil: null,
128
+ featuresSnapshot: [],
129
+ quotaEffectsSnapshot: {},
130
+ metadata: null
131
+ }
132
+ ]
133
+ };
134
+ }
41
135
  var CONTRACT_GAPS = {
42
136
  atomicPlanBinding: {
43
137
  reason: "adapter does not expose atomic plan-binding writes",
@@ -135,8 +229,16 @@ var CONTRACT_GAPS = {
135
229
  present: ({ adapter }) => Boolean(adapter.mfa)
136
230
  },
137
231
  subscriptionContracts: {
138
- reason: "adapter provides no SubscriptionContractRepository",
139
- present: ({ adapter }) => Boolean(adapter.subscriptionContractRepository)
232
+ reason: "adapter provides no SubscriptionContractRepository, or no subscriber seed for it",
233
+ present: ({ adapter, seed }) => Boolean(adapter.subscriptionContractRepository && seed.createSubscriber)
234
+ },
235
+ subscribers: {
236
+ reason: "adapter provides no SubscriberRepository",
237
+ present: ({ adapter }) => Boolean(adapter.subscriberRepository)
238
+ },
239
+ checkoutOffers: {
240
+ reason: "adapter provides no CheckoutOfferRepository",
241
+ present: ({ adapter }) => Boolean(adapter.checkoutOfferRepository)
140
242
  },
141
243
  appliedSettings: {
142
244
  reason: "adapter provides no AppliedSettingsPort",
@@ -1327,14 +1429,18 @@ function persistenceAdapterContract(options) {
1327
1429
  });
1328
1430
  (0, import_node_test.test)("a contract keeps what was agreed, and ending it does not rewrite it", async (t) => {
1329
1431
  const contracts = harness.adapter.subscriptionContractRepository;
1330
- if (!contracts) {
1432
+ const createSubscriber = harness.seed.createSubscriber;
1433
+ if (!contracts || !createSubscriber) {
1331
1434
  missing(t, "subscriptionContracts");
1332
1435
  return;
1333
1436
  }
1334
1437
  const tenantId = "tenant-contract-lifecycle";
1335
1438
  const signedAt = /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z");
1439
+ const { subscriberId } = await createSubscriber({ legalName: "Meier GmbH" });
1440
+ const parties = partiesWith(subscriberId, "Meier GmbH");
1336
1441
  const created = await contracts.create({
1337
1442
  tenantId,
1443
+ parties,
1338
1444
  effectiveFrom: signedAt,
1339
1445
  priceSnapshot: {
1340
1446
  currency: "EUR",
@@ -1395,6 +1501,10 @@ function persistenceAdapterContract(options) {
1395
1501
  });
1396
1502
  import_strict.default.equal(created.status, "active");
1397
1503
  import_strict.default.equal(created.tenantId, tenantId);
1504
+ import_strict.default.equal(created.subscriberId, subscriberId);
1505
+ import_strict.default.deepEqual(created.subscriber, parties.subscriber);
1506
+ import_strict.default.deepEqual(created.issuer, parties.issuer);
1507
+ import_strict.default.equal(created.partiesMigrated, false);
1398
1508
  import_strict.default.equal(created.lineItems.length, 2);
1399
1509
  import_strict.default.deepEqual(created.originalBundleVersionIds, ["bundle-version-1"]);
1400
1510
  import_strict.default.deepEqual(created.termsSnapshot, { noticePeriodDays: 30 });
@@ -1427,6 +1537,8 @@ function persistenceAdapterContract(options) {
1427
1537
  import_strict.default.equal(bundleLine.metadata, null);
1428
1538
  const readBack = await contracts.findById(created.id);
1429
1539
  import_strict.default.ok(readBack, "contract expected by id");
1540
+ import_strict.default.deepEqual(readBack.subscriber, parties.subscriber, "the copy is stored");
1541
+ import_strict.default.deepEqual(readBack.issuer, parties.issuer);
1430
1542
  import_strict.default.equal(
1431
1543
  readBack.lineItems.length,
1432
1544
  2,
@@ -1483,11 +1595,14 @@ function persistenceAdapterContract(options) {
1483
1595
  });
1484
1596
  (0, import_node_test.test)("a successor takes over without erasing the contract it replaces", async (t) => {
1485
1597
  const contracts = harness.adapter.subscriptionContractRepository;
1486
- if (!contracts) {
1598
+ const createSubscriber = harness.seed.createSubscriber;
1599
+ if (!contracts || !createSubscriber) {
1487
1600
  missing(t, "subscriptionContracts");
1488
1601
  return;
1489
1602
  }
1490
1603
  const tenantId = "tenant-contract-succession";
1604
+ const { subscriberId } = await createSubscriber({ legalName: "Nachfolger KG" });
1605
+ const parties = partiesWith(subscriberId, "Nachfolger KG");
1491
1606
  const handover = /* @__PURE__ */ new Date("2026-04-01T00:00:00.000Z");
1492
1607
  const lineAt = (priceNet, priceGross) => ({
1493
1608
  kind: "plan",
@@ -1519,6 +1634,7 @@ function persistenceAdapterContract(options) {
1519
1634
  });
1520
1635
  const first = await contracts.create({
1521
1636
  tenantId,
1637
+ parties,
1522
1638
  effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
1523
1639
  priceSnapshot: priceAt(19.9, 23.68),
1524
1640
  lineItems: [lineAt(19.9, 23.68)]
@@ -1536,6 +1652,7 @@ function persistenceAdapterContract(options) {
1536
1652
  });
1537
1653
  const second = await contracts.create({
1538
1654
  tenantId,
1655
+ parties,
1539
1656
  effectiveFrom: handover,
1540
1657
  priceSnapshot: priceAt(24.9, 29.63),
1541
1658
  lineItems: [lineAt(24.9, 29.63)]
@@ -1585,13 +1702,17 @@ function persistenceAdapterContract(options) {
1585
1702
  });
1586
1703
  (0, import_node_test.test)("a line keeps the currency and the tax it was booked with", async (t) => {
1587
1704
  const contracts = harness.adapter.subscriptionContractRepository;
1588
- if (!contracts) {
1705
+ const createSubscriber = harness.seed.createSubscriber;
1706
+ if (!contracts || !createSubscriber) {
1589
1707
  missing(t, "subscriptionContracts");
1590
1708
  return;
1591
1709
  }
1592
1710
  const tenantId = "tenant-contract-money-facts";
1711
+ const { subscriberId } = await createSubscriber({ legalName: "Z\xFCrich AG" });
1712
+ const parties = { ...partiesWith(subscriberId, "Z\xFCrich AG"), issuer: null };
1593
1713
  const created = await contracts.create({
1594
1714
  tenantId,
1715
+ parties,
1595
1716
  effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
1596
1717
  priceSnapshot: {
1597
1718
  currency: "CHF",
@@ -1665,6 +1786,414 @@ function persistenceAdapterContract(options) {
1665
1786
  );
1666
1787
  }
1667
1788
  });
1789
+ (0, import_node_test.test)("a contract written on a transaction is undone with it, and found by its offer", async (t) => {
1790
+ const { adapter, seed } = harness;
1791
+ const contracts = adapter.subscriptionContractRepository;
1792
+ if (!contracts || !seed.createSubscriber) {
1793
+ missing(t, "subscriptionContracts");
1794
+ return;
1795
+ }
1796
+ if (!adapter.capabilities.transactions) {
1797
+ t.skip("adapter declares no transaction capability");
1798
+ return;
1799
+ }
1800
+ const { subscriberId } = await seed.createSubscriber({ legalName: "Angebot GmbH" });
1801
+ const parties = partiesWith(subscriberId, "Angebot GmbH");
1802
+ await import_strict.default.rejects(
1803
+ adapter.transactionRunner.run(async (tx) => {
1804
+ await contracts.create(contractFromOffer("offer-rolled-back", parties), tx);
1805
+ throw new Error("the conclusion fails after the contract is written");
1806
+ })
1807
+ );
1808
+ import_strict.default.equal(
1809
+ await contracts.findByOriginalOfferId("offer-rolled-back"),
1810
+ null,
1811
+ "the contract outlived its transaction"
1812
+ );
1813
+ const kept = await adapter.transactionRunner.run(
1814
+ (tx) => contracts.create(contractFromOffer("offer-kept", parties), tx)
1815
+ );
1816
+ const found = await contracts.findByOriginalOfferId("offer-kept");
1817
+ import_strict.default.equal(found?.id, kept.id);
1818
+ import_strict.default.equal(found?.lineItems.length, 1, "with its lines");
1819
+ import_strict.default.equal(await contracts.findByOriginalOfferId("offer-nobody-concluded"), null);
1820
+ });
1821
+ (0, import_node_test.test)("a subscriber is created live for its tenant, and numbered by the database", async (t) => {
1822
+ const subscribers = harness.adapter.subscriberRepository;
1823
+ if (!subscribers) {
1824
+ missing(t, "subscribers");
1825
+ return;
1826
+ }
1827
+ const first = await subscribers.createForTenant({
1828
+ ...subscriberFor("tenant-subscriber-first", "Erste Autohaus GmbH"),
1829
+ vatId: "DE123456789",
1830
+ addressLine1: "Hauptstra\xDFe 1",
1831
+ postalCode: "10115",
1832
+ city: "Berlin",
1833
+ country: "DE",
1834
+ invoiceEmail: "rechnung@erste.example",
1835
+ customerNumberPrefix: "K-"
1836
+ });
1837
+ const second = await subscribers.createForTenant(
1838
+ subscriberFor("tenant-subscriber-second", "Zweiter Verein e.V.")
1839
+ );
1840
+ import_strict.default.ok(first && second, "both tenants had no subscriber");
1841
+ const numberOf = (customerNumber) => Number(customerNumber.replace("K-", ""));
1842
+ import_strict.default.ok(first.customerNumber.startsWith("K-"), first.customerNumber);
1843
+ import_strict.default.equal(numberOf(second.customerNumber), numberOf(first.customerNumber) + 1);
1844
+ import_strict.default.ok(numberOf(first.customerNumber) >= 10001, first.customerNumber);
1845
+ const found = await subscribers.findByTenantId("tenant-subscriber-first");
1846
+ import_strict.default.deepEqual(
1847
+ found && {
1848
+ id: found.id,
1849
+ customerNumber: found.customerNumber,
1850
+ tenantId: found.tenantId,
1851
+ legalName: found.legalName,
1852
+ vatId: found.vatId,
1853
+ taxNumber: found.taxNumber,
1854
+ addressLine1: found.addressLine1,
1855
+ addressLine2: found.addressLine2,
1856
+ postalCode: found.postalCode,
1857
+ city: found.city,
1858
+ country: found.country,
1859
+ invoiceEmail: found.invoiceEmail,
1860
+ migrated: found.migrated
1861
+ },
1862
+ {
1863
+ id: first.id,
1864
+ customerNumber: first.customerNumber,
1865
+ tenantId: "tenant-subscriber-first",
1866
+ legalName: "Erste Autohaus GmbH",
1867
+ vatId: "DE123456789",
1868
+ // The nullable half, so an adapter writing '' is caught too.
1869
+ taxNumber: null,
1870
+ addressLine1: "Hauptstra\xDFe 1",
1871
+ addressLine2: null,
1872
+ postalCode: "10115",
1873
+ city: "Berlin",
1874
+ country: "DE",
1875
+ invoiceEmail: "rechnung@erste.example",
1876
+ migrated: false
1877
+ }
1878
+ );
1879
+ import_strict.default.equal(
1880
+ (await subscribers.findById(second.id))?.tenantId,
1881
+ "tenant-subscriber-second"
1882
+ );
1883
+ import_strict.default.equal(await subscribers.findByTenantId("tenant-without-subscriber"), null);
1884
+ import_strict.default.equal(await subscribers.findById("subscriber-nobody-created"), null);
1885
+ });
1886
+ (0, import_node_test.test)("a tenant has one live subscriber: a second is refused, and the transaction survives it", async (t) => {
1887
+ const { adapter } = harness;
1888
+ const subscribers = adapter.subscriberRepository;
1889
+ if (!subscribers) {
1890
+ missing(t, "subscribers");
1891
+ return;
1892
+ }
1893
+ if (!adapter.capabilities.transactions) {
1894
+ t.skip("adapter declares no transaction capability");
1895
+ return;
1896
+ }
1897
+ const first = await subscribers.createForTenant(
1898
+ subscriberFor("tenant-one-subscriber", "Einzig GmbH")
1899
+ );
1900
+ const afterRefusal = await adapter.transactionRunner.run(async (tx) => {
1901
+ const refused = await subscribers.createForTenant(
1902
+ subscriberFor("tenant-one-subscriber", "Doppelt GmbH"),
1903
+ tx
1904
+ );
1905
+ import_strict.default.equal(refused, null, "a second live subscriber was created");
1906
+ return subscribers.createForTenant(
1907
+ subscriberFor("tenant-after-refusal", "Danach GmbH"),
1908
+ tx
1909
+ );
1910
+ });
1911
+ import_strict.default.equal(
1912
+ (await subscribers.findByTenantId("tenant-one-subscriber"))?.id,
1913
+ first?.id
1914
+ );
1915
+ import_strict.default.equal(
1916
+ (await subscribers.findByTenantId("tenant-after-refusal"))?.id,
1917
+ afterRefusal?.id,
1918
+ "the transaction did not survive the refusal"
1919
+ );
1920
+ });
1921
+ (0, import_node_test.test)("creating a subscriber for one tenant twice at once ends with one", async (t) => {
1922
+ const subscribers = harness.adapter.subscriberRepository;
1923
+ if (!subscribers) {
1924
+ missing(t, "subscribers");
1925
+ return;
1926
+ }
1927
+ const results = await Promise.all([
1928
+ subscribers.createForTenant(subscriberFor("tenant-at-once", "Gleichzeitig A")),
1929
+ subscribers.createForTenant(subscriberFor("tenant-at-once", "Gleichzeitig B"))
1930
+ ]);
1931
+ const created = results.filter((result) => result !== null);
1932
+ import_strict.default.equal(created.length, 1, `${created.length} subscribers were created`);
1933
+ import_strict.default.equal((await subscribers.findByTenantId("tenant-at-once"))?.id, created[0].id);
1934
+ });
1935
+ (0, import_node_test.test)("a subscriber written on a transaction is undone with it", async (t) => {
1936
+ const { adapter } = harness;
1937
+ const subscribers = adapter.subscriberRepository;
1938
+ if (!subscribers) {
1939
+ missing(t, "subscribers");
1940
+ return;
1941
+ }
1942
+ if (!adapter.capabilities.transactions) {
1943
+ t.skip("adapter declares no transaction capability");
1944
+ return;
1945
+ }
1946
+ await import_strict.default.rejects(
1947
+ adapter.transactionRunner.run(async (tx) => {
1948
+ await subscribers.createForTenant(
1949
+ subscriberFor("tenant-rolled-back", "Zur\xFCckgerollt GmbH"),
1950
+ tx
1951
+ );
1952
+ throw new Error("the tenant is not created after all");
1953
+ })
1954
+ );
1955
+ import_strict.default.equal(await subscribers.findByTenantId("tenant-rolled-back"), null);
1956
+ import_strict.default.ok(
1957
+ await subscribers.createForTenant(
1958
+ subscriberFor("tenant-rolled-back", "Zur\xFCckgerollt GmbH")
1959
+ )
1960
+ );
1961
+ });
1962
+ (0, import_node_test.test)("a contact change writes what it names and keeps the rest", async (t) => {
1963
+ const subscribers = harness.adapter.subscriberRepository;
1964
+ if (!subscribers) {
1965
+ missing(t, "subscribers");
1966
+ return;
1967
+ }
1968
+ const created = await subscribers.createForTenant({
1969
+ ...subscriberFor("tenant-contact", "Kontakt GmbH"),
1970
+ addressLine2: "Hinterhaus",
1971
+ city: "Hamburg"
1972
+ });
1973
+ import_strict.default.ok(created);
1974
+ const changed = await subscribers.updateContact(created.id, {
1975
+ city: "Bremen",
1976
+ addressLine2: null,
1977
+ invoiceEmail: "buchhaltung@kontakt.example"
1978
+ });
1979
+ import_strict.default.deepEqual(
1980
+ changed && [
1981
+ changed.city,
1982
+ changed.addressLine2,
1983
+ changed.invoiceEmail,
1984
+ changed.legalName,
1985
+ changed.tenantId
1986
+ ],
1987
+ ["Bremen", null, "buchhaltung@kontakt.example", "Kontakt GmbH", "tenant-contact"]
1988
+ );
1989
+ import_strict.default.equal((await subscribers.findById(created.id))?.city, "Bremen");
1990
+ import_strict.default.equal(
1991
+ await subscribers.updateContact("subscriber-nobody-created", { city: "Kiel" }),
1992
+ null
1993
+ );
1994
+ });
1995
+ (0, import_node_test.test)("a correction records the values it replaced, and nothing when nothing moves", async (t) => {
1996
+ const subscribers = harness.adapter.subscriberRepository;
1997
+ if (!subscribers) {
1998
+ missing(t, "subscribers");
1999
+ return;
2000
+ }
2001
+ const created = await subscribers.createForTenant(
2002
+ subscriberFor("tenant-correction", "Mueller GmbH")
2003
+ );
2004
+ import_strict.default.ok(created);
2005
+ const at = (day) => new Date(Date.UTC(2026, 8, day));
2006
+ const first = await subscribers.correctIdentity(created.id, {
2007
+ corrected: { legalName: "M\xFCller GmbH", vatId: "DE123456789", taxNumber: null },
2008
+ reason: "Umlaut lost when the registration was typed",
2009
+ correctedBy: "operator:anna",
2010
+ correctedAt: at(1)
2011
+ });
2012
+ import_strict.default.deepEqual(
2013
+ first?.correction && {
2014
+ previous: first.correction.previous,
2015
+ corrected: first.correction.corrected,
2016
+ reason: first.correction.reason,
2017
+ correctedBy: first.correction.correctedBy,
2018
+ correctedAt: first.correction.correctedAt.getTime()
2019
+ },
2020
+ {
2021
+ previous: { legalName: "Mueller GmbH", vatId: null },
2022
+ corrected: { legalName: "M\xFCller GmbH", vatId: "DE123456789" },
2023
+ reason: "Umlaut lost when the registration was typed",
2024
+ correctedBy: "operator:anna",
2025
+ correctedAt: at(1).getTime()
2026
+ }
2027
+ );
2028
+ import_strict.default.equal(first?.subscriber.legalName, "M\xFCller GmbH");
2029
+ import_strict.default.equal((await subscribers.findById(created.id))?.vatId, "DE123456789");
2030
+ const unchanged = await subscribers.correctIdentity(created.id, {
2031
+ corrected: { legalName: "M\xFCller GmbH" },
2032
+ reason: "Clicked twice",
2033
+ correctedBy: "operator:anna",
2034
+ correctedAt: at(2)
2035
+ });
2036
+ import_strict.default.equal(
2037
+ unchanged?.correction,
2038
+ null,
2039
+ "a correction that moved nothing was recorded"
2040
+ );
2041
+ await subscribers.correctIdentity(created.id, {
2042
+ corrected: { vatId: "DE999999999" },
2043
+ reason: "Wrong VAT id on the first correction",
2044
+ correctedBy: "operator:ben",
2045
+ correctedAt: at(3)
2046
+ });
2047
+ const listed = await subscribers.listCorrections(created.id);
2048
+ import_strict.default.deepEqual(
2049
+ listed.map((correction) => correction.reason),
2050
+ [
2051
+ "Wrong VAT id on the first correction",
2052
+ "Umlaut lost when the registration was typed"
2053
+ ],
2054
+ "corrections come back the latest first, and only the two that moved something"
2055
+ );
2056
+ import_strict.default.equal(
2057
+ await subscribers.correctIdentity("subscriber-nobody-created", {
2058
+ corrected: { legalName: "Niemand" },
2059
+ reason: "none",
2060
+ correctedBy: "operator:anna",
2061
+ correctedAt: at(4)
2062
+ }),
2063
+ null
2064
+ );
2065
+ });
2066
+ (0, import_node_test.test)("two corrections at once each record the value the other left behind", async (t) => {
2067
+ const { adapter } = harness;
2068
+ const subscribers = adapter.subscriberRepository;
2069
+ if (!subscribers) {
2070
+ missing(t, "subscribers");
2071
+ return;
2072
+ }
2073
+ if (!adapter.capabilities.transactions || !adapter.capabilities.pessimisticLocking) {
2074
+ t.skip("adapter declares no transactions or no row locks");
2075
+ return;
2076
+ }
2077
+ const created = await subscribers.createForTenant(
2078
+ subscriberFor("tenant-corrected-twice", "Original GmbH")
2079
+ );
2080
+ import_strict.default.ok(created);
2081
+ const correctTo = (legalName) => adapter.transactionRunner.run(async (tx) => {
2082
+ await subscribers.correctIdentity(
2083
+ created.id,
2084
+ {
2085
+ corrected: { legalName },
2086
+ reason: `to ${legalName}`,
2087
+ correctedBy: "operator:anna",
2088
+ correctedAt: /* @__PURE__ */ new Date()
2089
+ },
2090
+ tx
2091
+ );
2092
+ await sleep(LOCK_HOLD_MS);
2093
+ });
2094
+ await Promise.all([
2095
+ correctTo("Erste Korrektur GmbH"),
2096
+ correctTo("Zweite Korrektur GmbH")
2097
+ ]);
2098
+ const listed = await subscribers.listCorrections(created.id);
2099
+ import_strict.default.equal(listed.length, 2);
2100
+ const replaced = listed.map((correction) => correction.previous.legalName).sort();
2101
+ const written = listed.map((correction) => correction.corrected.legalName);
2102
+ const current = (await subscribers.findById(created.id))?.legalName;
2103
+ import_strict.default.ok(replaced.includes("Original GmbH"), JSON.stringify(listed));
2104
+ const secondReplaced = replaced.find((name) => name !== "Original GmbH");
2105
+ import_strict.default.ok(
2106
+ secondReplaced !== void 0 && written.includes(secondReplaced),
2107
+ `a correction recorded a value it did not replace: ${JSON.stringify(listed)}`
2108
+ );
2109
+ import_strict.default.ok(current !== void 0 && written.includes(current));
2110
+ import_strict.default.notEqual(secondReplaced, current);
2111
+ });
2112
+ (0, import_node_test.test)("a contract keeps its subscriber's copy after the subscriber is corrected", async (t) => {
2113
+ const { adapter } = harness;
2114
+ const subscribers = adapter.subscriberRepository;
2115
+ const contracts = adapter.subscriptionContractRepository;
2116
+ if (!subscribers) {
2117
+ missing(t, "subscribers");
2118
+ return;
2119
+ }
2120
+ if (!contracts || !harness.seed.createSubscriber) {
2121
+ missing(t, "subscriptionContracts");
2122
+ return;
2123
+ }
2124
+ const subscriber = await subscribers.createForTenant(
2125
+ subscriberFor("tenant-copy-kept", "Vorher GmbH")
2126
+ );
2127
+ import_strict.default.ok(subscriber);
2128
+ const parties = partiesWith(subscriber.id, "Vorher GmbH");
2129
+ const contract = await contracts.create({
2130
+ ...contractFromOffer("offer-copy-kept", parties),
2131
+ tenantId: "tenant-copy-kept"
2132
+ });
2133
+ await subscribers.correctIdentity(subscriber.id, {
2134
+ corrected: { legalName: "Nachher GmbH" },
2135
+ reason: "Change of name of the same company",
2136
+ correctedBy: "operator:anna",
2137
+ correctedAt: /* @__PURE__ */ new Date()
2138
+ });
2139
+ const readBack = await contracts.findById(contract.id);
2140
+ import_strict.default.equal(readBack?.subscriberId, subscriber.id);
2141
+ import_strict.default.equal(
2142
+ readBack?.subscriber.legalName,
2143
+ "Vorher GmbH",
2144
+ "the contract followed a correction of the live record"
2145
+ );
2146
+ });
2147
+ (0, import_node_test.test)("an offer is consumed once, whoever asks first", async (t) => {
2148
+ const offers = harness.adapter.checkoutOfferRepository;
2149
+ if (!offers) {
2150
+ missing(t, "checkoutOffers");
2151
+ return;
2152
+ }
2153
+ const offer = await offers.create(OFFER);
2154
+ import_strict.default.equal(offer.status, "open");
2155
+ const attempts = await Promise.allSettled([
2156
+ offers.consume(offer.id),
2157
+ offers.consume(offer.id)
2158
+ ]);
2159
+ import_strict.default.equal(
2160
+ attempts.filter((attempt) => attempt.status === "fulfilled").length,
2161
+ 1,
2162
+ "exactly one consume wins"
2163
+ );
2164
+ const consumed = await offers.findById(offer.id);
2165
+ import_strict.default.equal(consumed?.status, "consumed");
2166
+ import_strict.default.ok(consumed?.consumedAt, "and it records when");
2167
+ await import_strict.default.rejects(() => offers.consume(offer.id), "a later consume is refused");
2168
+ import_strict.default.equal(
2169
+ (await offers.findById(offer.id))?.consumedAt,
2170
+ consumed.consumedAt,
2171
+ "and changes nothing"
2172
+ );
2173
+ });
2174
+ (0, import_node_test.test)("a consume on a transaction that rolls back leaves the offer open", async (t) => {
2175
+ const { adapter } = harness;
2176
+ const offers = adapter.checkoutOfferRepository;
2177
+ if (!offers) {
2178
+ missing(t, "checkoutOffers");
2179
+ return;
2180
+ }
2181
+ if (!adapter.capabilities.transactions) {
2182
+ t.skip("adapter declares no transaction capability");
2183
+ return;
2184
+ }
2185
+ const offer = await offers.create(OFFER);
2186
+ await import_strict.default.rejects(
2187
+ adapter.transactionRunner.run(async (tx) => {
2188
+ await offers.consume(offer.id, tx);
2189
+ throw new Error("the contract after the consume fails");
2190
+ })
2191
+ );
2192
+ const afterwards = await offers.findById(offer.id);
2193
+ import_strict.default.equal(afterwards?.status, "open", "the consume outlived its transaction");
2194
+ import_strict.default.equal(afterwards?.consumedAt, null);
2195
+ import_strict.default.equal((await offers.consume(offer.id)).status, "consumed");
2196
+ });
1668
2197
  const SETTINGS = {
1669
2198
  app: { name: "Demo" },
1670
2199
  currency: "EUR",