@saasicat/persistence-testing 1.0.0-rc.15 → 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 +1 -1
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +403 -10
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +403 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ persistenceAdapterContract({
|
|
|
94
94
|
}),
|
|
95
95
|
// The parts this adapter deliberately does not provide. A part named here
|
|
96
96
|
// that the harness does provide fails the suite as well.
|
|
97
|
-
gaps: ['subscriptionContracts', 'checkoutOffers', 'appliedSettings'],
|
|
97
|
+
gaps: ['subscriptionContracts', 'subscribers', 'checkoutOffers', 'appliedSettings'],
|
|
98
98
|
});
|
|
99
99
|
```
|
|
100
100
|
|
package/dist/.build-stamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1ca79840c535585e92be3594dc7e1b240dfd47e78e98d7577fe19a1cd7e51faf
|
package/dist/index.cjs
CHANGED
|
@@ -53,9 +53,51 @@ var OFFER = {
|
|
|
53
53
|
effectiveGross: 58.31
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
|
-
function
|
|
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) {
|
|
57
98
|
return {
|
|
58
99
|
tenantId: `tenant-${offerId}`,
|
|
100
|
+
parties,
|
|
59
101
|
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
60
102
|
originalOfferId: offerId,
|
|
61
103
|
priceSnapshot: {
|
|
@@ -187,8 +229,12 @@ var CONTRACT_GAPS = {
|
|
|
187
229
|
present: ({ adapter }) => Boolean(adapter.mfa)
|
|
188
230
|
},
|
|
189
231
|
subscriptionContracts: {
|
|
190
|
-
reason: "adapter provides no SubscriptionContractRepository",
|
|
191
|
-
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)
|
|
192
238
|
},
|
|
193
239
|
checkoutOffers: {
|
|
194
240
|
reason: "adapter provides no CheckoutOfferRepository",
|
|
@@ -1383,14 +1429,18 @@ function persistenceAdapterContract(options) {
|
|
|
1383
1429
|
});
|
|
1384
1430
|
(0, import_node_test.test)("a contract keeps what was agreed, and ending it does not rewrite it", async (t) => {
|
|
1385
1431
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1386
|
-
|
|
1432
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
1433
|
+
if (!contracts || !createSubscriber) {
|
|
1387
1434
|
missing(t, "subscriptionContracts");
|
|
1388
1435
|
return;
|
|
1389
1436
|
}
|
|
1390
1437
|
const tenantId = "tenant-contract-lifecycle";
|
|
1391
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");
|
|
1392
1441
|
const created = await contracts.create({
|
|
1393
1442
|
tenantId,
|
|
1443
|
+
parties,
|
|
1394
1444
|
effectiveFrom: signedAt,
|
|
1395
1445
|
priceSnapshot: {
|
|
1396
1446
|
currency: "EUR",
|
|
@@ -1451,6 +1501,10 @@ function persistenceAdapterContract(options) {
|
|
|
1451
1501
|
});
|
|
1452
1502
|
import_strict.default.equal(created.status, "active");
|
|
1453
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);
|
|
1454
1508
|
import_strict.default.equal(created.lineItems.length, 2);
|
|
1455
1509
|
import_strict.default.deepEqual(created.originalBundleVersionIds, ["bundle-version-1"]);
|
|
1456
1510
|
import_strict.default.deepEqual(created.termsSnapshot, { noticePeriodDays: 30 });
|
|
@@ -1483,6 +1537,8 @@ function persistenceAdapterContract(options) {
|
|
|
1483
1537
|
import_strict.default.equal(bundleLine.metadata, null);
|
|
1484
1538
|
const readBack = await contracts.findById(created.id);
|
|
1485
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);
|
|
1486
1542
|
import_strict.default.equal(
|
|
1487
1543
|
readBack.lineItems.length,
|
|
1488
1544
|
2,
|
|
@@ -1539,11 +1595,14 @@ function persistenceAdapterContract(options) {
|
|
|
1539
1595
|
});
|
|
1540
1596
|
(0, import_node_test.test)("a successor takes over without erasing the contract it replaces", async (t) => {
|
|
1541
1597
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1542
|
-
|
|
1598
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
1599
|
+
if (!contracts || !createSubscriber) {
|
|
1543
1600
|
missing(t, "subscriptionContracts");
|
|
1544
1601
|
return;
|
|
1545
1602
|
}
|
|
1546
1603
|
const tenantId = "tenant-contract-succession";
|
|
1604
|
+
const { subscriberId } = await createSubscriber({ legalName: "Nachfolger KG" });
|
|
1605
|
+
const parties = partiesWith(subscriberId, "Nachfolger KG");
|
|
1547
1606
|
const handover = /* @__PURE__ */ new Date("2026-04-01T00:00:00.000Z");
|
|
1548
1607
|
const lineAt = (priceNet, priceGross) => ({
|
|
1549
1608
|
kind: "plan",
|
|
@@ -1575,6 +1634,7 @@ function persistenceAdapterContract(options) {
|
|
|
1575
1634
|
});
|
|
1576
1635
|
const first = await contracts.create({
|
|
1577
1636
|
tenantId,
|
|
1637
|
+
parties,
|
|
1578
1638
|
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
1579
1639
|
priceSnapshot: priceAt(19.9, 23.68),
|
|
1580
1640
|
lineItems: [lineAt(19.9, 23.68)]
|
|
@@ -1592,6 +1652,7 @@ function persistenceAdapterContract(options) {
|
|
|
1592
1652
|
});
|
|
1593
1653
|
const second = await contracts.create({
|
|
1594
1654
|
tenantId,
|
|
1655
|
+
parties,
|
|
1595
1656
|
effectiveFrom: handover,
|
|
1596
1657
|
priceSnapshot: priceAt(24.9, 29.63),
|
|
1597
1658
|
lineItems: [lineAt(24.9, 29.63)]
|
|
@@ -1641,13 +1702,17 @@ function persistenceAdapterContract(options) {
|
|
|
1641
1702
|
});
|
|
1642
1703
|
(0, import_node_test.test)("a line keeps the currency and the tax it was booked with", async (t) => {
|
|
1643
1704
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1644
|
-
|
|
1705
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
1706
|
+
if (!contracts || !createSubscriber) {
|
|
1645
1707
|
missing(t, "subscriptionContracts");
|
|
1646
1708
|
return;
|
|
1647
1709
|
}
|
|
1648
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 };
|
|
1649
1713
|
const created = await contracts.create({
|
|
1650
1714
|
tenantId,
|
|
1715
|
+
parties,
|
|
1651
1716
|
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
1652
1717
|
priceSnapshot: {
|
|
1653
1718
|
currency: "CHF",
|
|
@@ -1722,9 +1787,9 @@ function persistenceAdapterContract(options) {
|
|
|
1722
1787
|
}
|
|
1723
1788
|
});
|
|
1724
1789
|
(0, import_node_test.test)("a contract written on a transaction is undone with it, and found by its offer", async (t) => {
|
|
1725
|
-
const { adapter } = harness;
|
|
1790
|
+
const { adapter, seed } = harness;
|
|
1726
1791
|
const contracts = adapter.subscriptionContractRepository;
|
|
1727
|
-
if (!contracts) {
|
|
1792
|
+
if (!contracts || !seed.createSubscriber) {
|
|
1728
1793
|
missing(t, "subscriptionContracts");
|
|
1729
1794
|
return;
|
|
1730
1795
|
}
|
|
@@ -1732,9 +1797,11 @@ function persistenceAdapterContract(options) {
|
|
|
1732
1797
|
t.skip("adapter declares no transaction capability");
|
|
1733
1798
|
return;
|
|
1734
1799
|
}
|
|
1800
|
+
const { subscriberId } = await seed.createSubscriber({ legalName: "Angebot GmbH" });
|
|
1801
|
+
const parties = partiesWith(subscriberId, "Angebot GmbH");
|
|
1735
1802
|
await import_strict.default.rejects(
|
|
1736
1803
|
adapter.transactionRunner.run(async (tx) => {
|
|
1737
|
-
await contracts.create(contractFromOffer("offer-rolled-back"), tx);
|
|
1804
|
+
await contracts.create(contractFromOffer("offer-rolled-back", parties), tx);
|
|
1738
1805
|
throw new Error("the conclusion fails after the contract is written");
|
|
1739
1806
|
})
|
|
1740
1807
|
);
|
|
@@ -1744,13 +1811,339 @@ function persistenceAdapterContract(options) {
|
|
|
1744
1811
|
"the contract outlived its transaction"
|
|
1745
1812
|
);
|
|
1746
1813
|
const kept = await adapter.transactionRunner.run(
|
|
1747
|
-
(tx) => contracts.create(contractFromOffer("offer-kept"), tx)
|
|
1814
|
+
(tx) => contracts.create(contractFromOffer("offer-kept", parties), tx)
|
|
1748
1815
|
);
|
|
1749
1816
|
const found = await contracts.findByOriginalOfferId("offer-kept");
|
|
1750
1817
|
import_strict.default.equal(found?.id, kept.id);
|
|
1751
1818
|
import_strict.default.equal(found?.lineItems.length, 1, "with its lines");
|
|
1752
1819
|
import_strict.default.equal(await contracts.findByOriginalOfferId("offer-nobody-concluded"), null);
|
|
1753
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
|
+
});
|
|
1754
2147
|
(0, import_node_test.test)("an offer is consumed once, whoever asks first", async (t) => {
|
|
1755
2148
|
const offers = harness.adapter.checkoutOfferRepository;
|
|
1756
2149
|
if (!offers) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PersistenceCapabilities, TransactionRunner, SubscriptionRepository, PlanVersionRepository, PromoCodeRepository, PromoCodeRedemptionRepository, MfaPort, AuditPort, AuditQueryPort, SubscriptionContractRepository, CheckoutOfferRepository, TenantSubscriptionWritePort, BundleRepository, SubscriptionBundleRepository, PlanRepository, PromoSubscriptionLookup, AppliedSettingsPort } from '@saasicat/core';
|
|
1
|
+
import { PersistenceCapabilities, TransactionRunner, SubscriptionRepository, PlanVersionRepository, PromoCodeRepository, PromoCodeRedemptionRepository, MfaPort, AuditPort, AuditQueryPort, SubscriptionContractRepository, SubscriberRepository, CheckoutOfferRepository, TenantSubscriptionWritePort, BundleRepository, SubscriptionBundleRepository, PlanRepository, PromoSubscriptionLookup, AppliedSettingsPort } from '@saasicat/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Port instances under test. Required members define the minimum an adapter
|
|
@@ -18,6 +18,14 @@ interface ContractAdapterInstances {
|
|
|
18
18
|
audit?: AuditPort;
|
|
19
19
|
auditQuery?: AuditQueryPort;
|
|
20
20
|
subscriptionContractRepository?: SubscriptionContractRepository;
|
|
21
|
+
/**
|
|
22
|
+
* Enables the subscriber scenarios: a customer number the database
|
|
23
|
+
* counts, one live subscriber per tenant however many callers create one
|
|
24
|
+
* at once, and a correction that records the values it replaced. A
|
|
25
|
+
* contract names its subscriber, so the contract scenarios write theirs
|
|
26
|
+
* through `seed.createSubscriber` rather than through this port.
|
|
27
|
+
*/
|
|
28
|
+
subscriberRepository?: SubscriberRepository;
|
|
21
29
|
/**
|
|
22
30
|
* Enables the checkout offer scenarios: an offer is consumed once, and a
|
|
23
31
|
* consume on a transaction that rolls back leaves it open. Neither shipped
|
|
@@ -124,6 +132,19 @@ interface ContractSeed {
|
|
|
124
132
|
}): Promise<{
|
|
125
133
|
promoCodeId: string;
|
|
126
134
|
}>;
|
|
135
|
+
/**
|
|
136
|
+
* A subscriber row for a contract scenario to name, linked to no tenant.
|
|
137
|
+
*
|
|
138
|
+
* A fixture writer for the reason `createBundleVersion` is one: the
|
|
139
|
+
* subscriber repository is a subject of its own scenarios, and a contract
|
|
140
|
+
* scenario that failed because of it would say the wrong thing. Optional
|
|
141
|
+
* like the port it stands beside; the contract scenarios report it missing.
|
|
142
|
+
*/
|
|
143
|
+
createSubscriber?(input: {
|
|
144
|
+
legalName: string;
|
|
145
|
+
}): Promise<{
|
|
146
|
+
subscriberId: string;
|
|
147
|
+
}>;
|
|
127
148
|
}
|
|
128
149
|
interface PersistenceContractHarness {
|
|
129
150
|
adapter: ContractAdapterInstances;
|
|
@@ -139,7 +160,7 @@ interface PersistenceContractHarness {
|
|
|
139
160
|
* Each names the members its scenarios need; `contract.ts` holds the list with
|
|
140
161
|
* what each one checks.
|
|
141
162
|
*/
|
|
142
|
-
type ContractGap = 'atomicPlanBinding' | 'atomicOnboarding' | 'promoCodes' | 'promoCodeRedemptions' | 'promoSubscriptionLookup' | 'planRepository' | 'planLifecycle' | 'planRetirement' | 'planVersionReads' | 'planVersionRetirement' | 'bundleRepository' | 'bundleValidity' | 'bundleDraftDiscard' | 'bundleDraftPublish' | 'bundleRetirement' | 'bundleBookings' | 'halfCancelledBookingSeed' | 'countByPlanVersionId' | 'audit' | 'mfa' | 'subscriptionContracts' | 'checkoutOffers' | 'appliedSettings';
|
|
163
|
+
type ContractGap = 'atomicPlanBinding' | 'atomicOnboarding' | 'promoCodes' | 'promoCodeRedemptions' | 'promoSubscriptionLookup' | 'planRepository' | 'planLifecycle' | 'planRetirement' | 'planVersionReads' | 'planVersionRetirement' | 'bundleRepository' | 'bundleValidity' | 'bundleDraftDiscard' | 'bundleDraftPublish' | 'bundleRetirement' | 'bundleBookings' | 'halfCancelledBookingSeed' | 'countByPlanVersionId' | 'audit' | 'mfa' | 'subscriptionContracts' | 'subscribers' | 'checkoutOffers' | 'appliedSettings';
|
|
143
164
|
interface PersistenceAdapterContractOptions {
|
|
144
165
|
/** Display name in the test output, e.g. `'adapter-prisma @ postgres16'`. */
|
|
145
166
|
name: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PersistenceCapabilities, TransactionRunner, SubscriptionRepository, PlanVersionRepository, PromoCodeRepository, PromoCodeRedemptionRepository, MfaPort, AuditPort, AuditQueryPort, SubscriptionContractRepository, CheckoutOfferRepository, TenantSubscriptionWritePort, BundleRepository, SubscriptionBundleRepository, PlanRepository, PromoSubscriptionLookup, AppliedSettingsPort } from '@saasicat/core';
|
|
1
|
+
import { PersistenceCapabilities, TransactionRunner, SubscriptionRepository, PlanVersionRepository, PromoCodeRepository, PromoCodeRedemptionRepository, MfaPort, AuditPort, AuditQueryPort, SubscriptionContractRepository, SubscriberRepository, CheckoutOfferRepository, TenantSubscriptionWritePort, BundleRepository, SubscriptionBundleRepository, PlanRepository, PromoSubscriptionLookup, AppliedSettingsPort } from '@saasicat/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Port instances under test. Required members define the minimum an adapter
|
|
@@ -18,6 +18,14 @@ interface ContractAdapterInstances {
|
|
|
18
18
|
audit?: AuditPort;
|
|
19
19
|
auditQuery?: AuditQueryPort;
|
|
20
20
|
subscriptionContractRepository?: SubscriptionContractRepository;
|
|
21
|
+
/**
|
|
22
|
+
* Enables the subscriber scenarios: a customer number the database
|
|
23
|
+
* counts, one live subscriber per tenant however many callers create one
|
|
24
|
+
* at once, and a correction that records the values it replaced. A
|
|
25
|
+
* contract names its subscriber, so the contract scenarios write theirs
|
|
26
|
+
* through `seed.createSubscriber` rather than through this port.
|
|
27
|
+
*/
|
|
28
|
+
subscriberRepository?: SubscriberRepository;
|
|
21
29
|
/**
|
|
22
30
|
* Enables the checkout offer scenarios: an offer is consumed once, and a
|
|
23
31
|
* consume on a transaction that rolls back leaves it open. Neither shipped
|
|
@@ -124,6 +132,19 @@ interface ContractSeed {
|
|
|
124
132
|
}): Promise<{
|
|
125
133
|
promoCodeId: string;
|
|
126
134
|
}>;
|
|
135
|
+
/**
|
|
136
|
+
* A subscriber row for a contract scenario to name, linked to no tenant.
|
|
137
|
+
*
|
|
138
|
+
* A fixture writer for the reason `createBundleVersion` is one: the
|
|
139
|
+
* subscriber repository is a subject of its own scenarios, and a contract
|
|
140
|
+
* scenario that failed because of it would say the wrong thing. Optional
|
|
141
|
+
* like the port it stands beside; the contract scenarios report it missing.
|
|
142
|
+
*/
|
|
143
|
+
createSubscriber?(input: {
|
|
144
|
+
legalName: string;
|
|
145
|
+
}): Promise<{
|
|
146
|
+
subscriberId: string;
|
|
147
|
+
}>;
|
|
127
148
|
}
|
|
128
149
|
interface PersistenceContractHarness {
|
|
129
150
|
adapter: ContractAdapterInstances;
|
|
@@ -139,7 +160,7 @@ interface PersistenceContractHarness {
|
|
|
139
160
|
* Each names the members its scenarios need; `contract.ts` holds the list with
|
|
140
161
|
* what each one checks.
|
|
141
162
|
*/
|
|
142
|
-
type ContractGap = 'atomicPlanBinding' | 'atomicOnboarding' | 'promoCodes' | 'promoCodeRedemptions' | 'promoSubscriptionLookup' | 'planRepository' | 'planLifecycle' | 'planRetirement' | 'planVersionReads' | 'planVersionRetirement' | 'bundleRepository' | 'bundleValidity' | 'bundleDraftDiscard' | 'bundleDraftPublish' | 'bundleRetirement' | 'bundleBookings' | 'halfCancelledBookingSeed' | 'countByPlanVersionId' | 'audit' | 'mfa' | 'subscriptionContracts' | 'checkoutOffers' | 'appliedSettings';
|
|
163
|
+
type ContractGap = 'atomicPlanBinding' | 'atomicOnboarding' | 'promoCodes' | 'promoCodeRedemptions' | 'promoSubscriptionLookup' | 'planRepository' | 'planLifecycle' | 'planRetirement' | 'planVersionReads' | 'planVersionRetirement' | 'bundleRepository' | 'bundleValidity' | 'bundleDraftDiscard' | 'bundleDraftPublish' | 'bundleRetirement' | 'bundleBookings' | 'halfCancelledBookingSeed' | 'countByPlanVersionId' | 'audit' | 'mfa' | 'subscriptionContracts' | 'subscribers' | 'checkoutOffers' | 'appliedSettings';
|
|
143
164
|
interface PersistenceAdapterContractOptions {
|
|
144
165
|
/** Display name in the test output, e.g. `'adapter-prisma @ postgres16'`. */
|
|
145
166
|
name: string;
|
package/dist/index.js
CHANGED
|
@@ -17,9 +17,51 @@ var OFFER = {
|
|
|
17
17
|
effectiveGross: 58.31
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
-
function
|
|
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) {
|
|
21
62
|
return {
|
|
22
63
|
tenantId: `tenant-${offerId}`,
|
|
64
|
+
parties,
|
|
23
65
|
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
24
66
|
originalOfferId: offerId,
|
|
25
67
|
priceSnapshot: {
|
|
@@ -151,8 +193,12 @@ var CONTRACT_GAPS = {
|
|
|
151
193
|
present: ({ adapter }) => Boolean(adapter.mfa)
|
|
152
194
|
},
|
|
153
195
|
subscriptionContracts: {
|
|
154
|
-
reason: "adapter provides no SubscriptionContractRepository",
|
|
155
|
-
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)
|
|
156
202
|
},
|
|
157
203
|
checkoutOffers: {
|
|
158
204
|
reason: "adapter provides no CheckoutOfferRepository",
|
|
@@ -1347,14 +1393,18 @@ function persistenceAdapterContract(options) {
|
|
|
1347
1393
|
});
|
|
1348
1394
|
test("a contract keeps what was agreed, and ending it does not rewrite it", async (t) => {
|
|
1349
1395
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1350
|
-
|
|
1396
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
1397
|
+
if (!contracts || !createSubscriber) {
|
|
1351
1398
|
missing(t, "subscriptionContracts");
|
|
1352
1399
|
return;
|
|
1353
1400
|
}
|
|
1354
1401
|
const tenantId = "tenant-contract-lifecycle";
|
|
1355
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");
|
|
1356
1405
|
const created = await contracts.create({
|
|
1357
1406
|
tenantId,
|
|
1407
|
+
parties,
|
|
1358
1408
|
effectiveFrom: signedAt,
|
|
1359
1409
|
priceSnapshot: {
|
|
1360
1410
|
currency: "EUR",
|
|
@@ -1415,6 +1465,10 @@ function persistenceAdapterContract(options) {
|
|
|
1415
1465
|
});
|
|
1416
1466
|
assert.equal(created.status, "active");
|
|
1417
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);
|
|
1418
1472
|
assert.equal(created.lineItems.length, 2);
|
|
1419
1473
|
assert.deepEqual(created.originalBundleVersionIds, ["bundle-version-1"]);
|
|
1420
1474
|
assert.deepEqual(created.termsSnapshot, { noticePeriodDays: 30 });
|
|
@@ -1447,6 +1501,8 @@ function persistenceAdapterContract(options) {
|
|
|
1447
1501
|
assert.equal(bundleLine.metadata, null);
|
|
1448
1502
|
const readBack = await contracts.findById(created.id);
|
|
1449
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);
|
|
1450
1506
|
assert.equal(
|
|
1451
1507
|
readBack.lineItems.length,
|
|
1452
1508
|
2,
|
|
@@ -1503,11 +1559,14 @@ function persistenceAdapterContract(options) {
|
|
|
1503
1559
|
});
|
|
1504
1560
|
test("a successor takes over without erasing the contract it replaces", async (t) => {
|
|
1505
1561
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1506
|
-
|
|
1562
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
1563
|
+
if (!contracts || !createSubscriber) {
|
|
1507
1564
|
missing(t, "subscriptionContracts");
|
|
1508
1565
|
return;
|
|
1509
1566
|
}
|
|
1510
1567
|
const tenantId = "tenant-contract-succession";
|
|
1568
|
+
const { subscriberId } = await createSubscriber({ legalName: "Nachfolger KG" });
|
|
1569
|
+
const parties = partiesWith(subscriberId, "Nachfolger KG");
|
|
1511
1570
|
const handover = /* @__PURE__ */ new Date("2026-04-01T00:00:00.000Z");
|
|
1512
1571
|
const lineAt = (priceNet, priceGross) => ({
|
|
1513
1572
|
kind: "plan",
|
|
@@ -1539,6 +1598,7 @@ function persistenceAdapterContract(options) {
|
|
|
1539
1598
|
});
|
|
1540
1599
|
const first = await contracts.create({
|
|
1541
1600
|
tenantId,
|
|
1601
|
+
parties,
|
|
1542
1602
|
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
1543
1603
|
priceSnapshot: priceAt(19.9, 23.68),
|
|
1544
1604
|
lineItems: [lineAt(19.9, 23.68)]
|
|
@@ -1556,6 +1616,7 @@ function persistenceAdapterContract(options) {
|
|
|
1556
1616
|
});
|
|
1557
1617
|
const second = await contracts.create({
|
|
1558
1618
|
tenantId,
|
|
1619
|
+
parties,
|
|
1559
1620
|
effectiveFrom: handover,
|
|
1560
1621
|
priceSnapshot: priceAt(24.9, 29.63),
|
|
1561
1622
|
lineItems: [lineAt(24.9, 29.63)]
|
|
@@ -1605,13 +1666,17 @@ function persistenceAdapterContract(options) {
|
|
|
1605
1666
|
});
|
|
1606
1667
|
test("a line keeps the currency and the tax it was booked with", async (t) => {
|
|
1607
1668
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1608
|
-
|
|
1669
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
1670
|
+
if (!contracts || !createSubscriber) {
|
|
1609
1671
|
missing(t, "subscriptionContracts");
|
|
1610
1672
|
return;
|
|
1611
1673
|
}
|
|
1612
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 };
|
|
1613
1677
|
const created = await contracts.create({
|
|
1614
1678
|
tenantId,
|
|
1679
|
+
parties,
|
|
1615
1680
|
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
1616
1681
|
priceSnapshot: {
|
|
1617
1682
|
currency: "CHF",
|
|
@@ -1686,9 +1751,9 @@ function persistenceAdapterContract(options) {
|
|
|
1686
1751
|
}
|
|
1687
1752
|
});
|
|
1688
1753
|
test("a contract written on a transaction is undone with it, and found by its offer", async (t) => {
|
|
1689
|
-
const { adapter } = harness;
|
|
1754
|
+
const { adapter, seed } = harness;
|
|
1690
1755
|
const contracts = adapter.subscriptionContractRepository;
|
|
1691
|
-
if (!contracts) {
|
|
1756
|
+
if (!contracts || !seed.createSubscriber) {
|
|
1692
1757
|
missing(t, "subscriptionContracts");
|
|
1693
1758
|
return;
|
|
1694
1759
|
}
|
|
@@ -1696,9 +1761,11 @@ function persistenceAdapterContract(options) {
|
|
|
1696
1761
|
t.skip("adapter declares no transaction capability");
|
|
1697
1762
|
return;
|
|
1698
1763
|
}
|
|
1764
|
+
const { subscriberId } = await seed.createSubscriber({ legalName: "Angebot GmbH" });
|
|
1765
|
+
const parties = partiesWith(subscriberId, "Angebot GmbH");
|
|
1699
1766
|
await assert.rejects(
|
|
1700
1767
|
adapter.transactionRunner.run(async (tx) => {
|
|
1701
|
-
await contracts.create(contractFromOffer("offer-rolled-back"), tx);
|
|
1768
|
+
await contracts.create(contractFromOffer("offer-rolled-back", parties), tx);
|
|
1702
1769
|
throw new Error("the conclusion fails after the contract is written");
|
|
1703
1770
|
})
|
|
1704
1771
|
);
|
|
@@ -1708,13 +1775,339 @@ function persistenceAdapterContract(options) {
|
|
|
1708
1775
|
"the contract outlived its transaction"
|
|
1709
1776
|
);
|
|
1710
1777
|
const kept = await adapter.transactionRunner.run(
|
|
1711
|
-
(tx) => contracts.create(contractFromOffer("offer-kept"), tx)
|
|
1778
|
+
(tx) => contracts.create(contractFromOffer("offer-kept", parties), tx)
|
|
1712
1779
|
);
|
|
1713
1780
|
const found = await contracts.findByOriginalOfferId("offer-kept");
|
|
1714
1781
|
assert.equal(found?.id, kept.id);
|
|
1715
1782
|
assert.equal(found?.lineItems.length, 1, "with its lines");
|
|
1716
1783
|
assert.equal(await contracts.findByOriginalOfferId("offer-nobody-concluded"), null);
|
|
1717
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
|
+
});
|
|
1718
2111
|
test("an offer is consumed once, whoever asks first", async (t) => {
|
|
1719
2112
|
const offers = harness.adapter.checkoutOfferRepository;
|
|
1720
2113
|
if (!offers) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/persistence-testing",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
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.
|
|
26
|
+
"@saasicat/core": "^1.0.0-rc.16"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^25.6.0",
|