@saasicat/persistence-testing 1.0.0-rc.15 → 1.0.0-rc.17
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 +14 -1
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +922 -10
- package/dist/index.d.cts +36 -2
- package/dist/index.d.ts +36 -2
- package/dist/index.js +922 -10
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -53,9 +53,84 @@ 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 eventAt(gatewayAccount, eventId, about = {}) {
|
|
83
|
+
return {
|
|
84
|
+
gatewayAccount,
|
|
85
|
+
eventId,
|
|
86
|
+
provider: "stripe",
|
|
87
|
+
sessionId: `cs_${eventId}`,
|
|
88
|
+
kind: "payment-method-confirmed",
|
|
89
|
+
summary: { type: "card", last4: "4242" },
|
|
90
|
+
...about
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
var CARD = {
|
|
94
|
+
type: "card",
|
|
95
|
+
brand: "visa",
|
|
96
|
+
last4: "4242",
|
|
97
|
+
expiryMonth: 12,
|
|
98
|
+
expiryYear: 2030,
|
|
99
|
+
country: null,
|
|
100
|
+
bankCode: null,
|
|
101
|
+
mandateReference: null,
|
|
102
|
+
customerRef: "cus_1",
|
|
103
|
+
paymentMethodRef: "pm_card"
|
|
104
|
+
};
|
|
105
|
+
function paymentMethodFor(subscriberId, paymentMethodRef, confirmedAt, gatewayAccount = "stripe-main") {
|
|
106
|
+
return {
|
|
107
|
+
...CARD,
|
|
108
|
+
paymentMethodRef,
|
|
109
|
+
subscriberId,
|
|
110
|
+
gatewayAccount,
|
|
111
|
+
provider: "stripe",
|
|
112
|
+
confirmedAt: new Date(confirmedAt)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function subscriberFor(tenantId, legalName) {
|
|
116
|
+
return {
|
|
117
|
+
tenantId,
|
|
118
|
+
legalName,
|
|
119
|
+
vatId: null,
|
|
120
|
+
taxNumber: null,
|
|
121
|
+
addressLine1: null,
|
|
122
|
+
addressLine2: null,
|
|
123
|
+
postalCode: null,
|
|
124
|
+
city: null,
|
|
125
|
+
country: null,
|
|
126
|
+
invoiceEmail: null,
|
|
127
|
+
customerNumberPrefix: ""
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function contractFromOffer(offerId, parties) {
|
|
57
131
|
return {
|
|
58
132
|
tenantId: `tenant-${offerId}`,
|
|
133
|
+
parties,
|
|
59
134
|
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
60
135
|
originalOfferId: offerId,
|
|
61
136
|
priceSnapshot: {
|
|
@@ -187,8 +262,20 @@ var CONTRACT_GAPS = {
|
|
|
187
262
|
present: ({ adapter }) => Boolean(adapter.mfa)
|
|
188
263
|
},
|
|
189
264
|
subscriptionContracts: {
|
|
190
|
-
reason: "adapter provides no SubscriptionContractRepository",
|
|
191
|
-
present: ({ adapter }) => Boolean(adapter.subscriptionContractRepository)
|
|
265
|
+
reason: "adapter provides no SubscriptionContractRepository, or no subscriber seed for it",
|
|
266
|
+
present: ({ adapter, seed }) => Boolean(adapter.subscriptionContractRepository && seed.createSubscriber)
|
|
267
|
+
},
|
|
268
|
+
subscribers: {
|
|
269
|
+
reason: "adapter provides no SubscriberRepository",
|
|
270
|
+
present: ({ adapter }) => Boolean(adapter.subscriberRepository)
|
|
271
|
+
},
|
|
272
|
+
paymentEventLog: {
|
|
273
|
+
reason: "adapter provides no PaymentEventLog",
|
|
274
|
+
present: ({ adapter }) => Boolean(adapter.paymentEventLog)
|
|
275
|
+
},
|
|
276
|
+
subscriberPaymentMethods: {
|
|
277
|
+
reason: "adapter provides no SubscriberPaymentMethodRepository, or no subscriber seed for it",
|
|
278
|
+
present: ({ adapter, seed }) => Boolean(adapter.subscriberPaymentMethodRepository && seed.createSubscriber)
|
|
192
279
|
},
|
|
193
280
|
checkoutOffers: {
|
|
194
281
|
reason: "adapter provides no CheckoutOfferRepository",
|
|
@@ -1383,14 +1470,18 @@ function persistenceAdapterContract(options) {
|
|
|
1383
1470
|
});
|
|
1384
1471
|
(0, import_node_test.test)("a contract keeps what was agreed, and ending it does not rewrite it", async (t) => {
|
|
1385
1472
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1386
|
-
|
|
1473
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
1474
|
+
if (!contracts || !createSubscriber) {
|
|
1387
1475
|
missing(t, "subscriptionContracts");
|
|
1388
1476
|
return;
|
|
1389
1477
|
}
|
|
1390
1478
|
const tenantId = "tenant-contract-lifecycle";
|
|
1391
1479
|
const signedAt = /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z");
|
|
1480
|
+
const { subscriberId } = await createSubscriber({ legalName: "Meier GmbH" });
|
|
1481
|
+
const parties = partiesWith(subscriberId, "Meier GmbH");
|
|
1392
1482
|
const created = await contracts.create({
|
|
1393
1483
|
tenantId,
|
|
1484
|
+
parties,
|
|
1394
1485
|
effectiveFrom: signedAt,
|
|
1395
1486
|
priceSnapshot: {
|
|
1396
1487
|
currency: "EUR",
|
|
@@ -1451,6 +1542,10 @@ function persistenceAdapterContract(options) {
|
|
|
1451
1542
|
});
|
|
1452
1543
|
import_strict.default.equal(created.status, "active");
|
|
1453
1544
|
import_strict.default.equal(created.tenantId, tenantId);
|
|
1545
|
+
import_strict.default.equal(created.subscriberId, subscriberId);
|
|
1546
|
+
import_strict.default.deepEqual(created.subscriber, parties.subscriber);
|
|
1547
|
+
import_strict.default.deepEqual(created.issuer, parties.issuer);
|
|
1548
|
+
import_strict.default.equal(created.partiesMigrated, false);
|
|
1454
1549
|
import_strict.default.equal(created.lineItems.length, 2);
|
|
1455
1550
|
import_strict.default.deepEqual(created.originalBundleVersionIds, ["bundle-version-1"]);
|
|
1456
1551
|
import_strict.default.deepEqual(created.termsSnapshot, { noticePeriodDays: 30 });
|
|
@@ -1483,6 +1578,8 @@ function persistenceAdapterContract(options) {
|
|
|
1483
1578
|
import_strict.default.equal(bundleLine.metadata, null);
|
|
1484
1579
|
const readBack = await contracts.findById(created.id);
|
|
1485
1580
|
import_strict.default.ok(readBack, "contract expected by id");
|
|
1581
|
+
import_strict.default.deepEqual(readBack.subscriber, parties.subscriber, "the copy is stored");
|
|
1582
|
+
import_strict.default.deepEqual(readBack.issuer, parties.issuer);
|
|
1486
1583
|
import_strict.default.equal(
|
|
1487
1584
|
readBack.lineItems.length,
|
|
1488
1585
|
2,
|
|
@@ -1539,11 +1636,14 @@ function persistenceAdapterContract(options) {
|
|
|
1539
1636
|
});
|
|
1540
1637
|
(0, import_node_test.test)("a successor takes over without erasing the contract it replaces", async (t) => {
|
|
1541
1638
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1542
|
-
|
|
1639
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
1640
|
+
if (!contracts || !createSubscriber) {
|
|
1543
1641
|
missing(t, "subscriptionContracts");
|
|
1544
1642
|
return;
|
|
1545
1643
|
}
|
|
1546
1644
|
const tenantId = "tenant-contract-succession";
|
|
1645
|
+
const { subscriberId } = await createSubscriber({ legalName: "Nachfolger KG" });
|
|
1646
|
+
const parties = partiesWith(subscriberId, "Nachfolger KG");
|
|
1547
1647
|
const handover = /* @__PURE__ */ new Date("2026-04-01T00:00:00.000Z");
|
|
1548
1648
|
const lineAt = (priceNet, priceGross) => ({
|
|
1549
1649
|
kind: "plan",
|
|
@@ -1575,6 +1675,7 @@ function persistenceAdapterContract(options) {
|
|
|
1575
1675
|
});
|
|
1576
1676
|
const first = await contracts.create({
|
|
1577
1677
|
tenantId,
|
|
1678
|
+
parties,
|
|
1578
1679
|
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
1579
1680
|
priceSnapshot: priceAt(19.9, 23.68),
|
|
1580
1681
|
lineItems: [lineAt(19.9, 23.68)]
|
|
@@ -1592,6 +1693,7 @@ function persistenceAdapterContract(options) {
|
|
|
1592
1693
|
});
|
|
1593
1694
|
const second = await contracts.create({
|
|
1594
1695
|
tenantId,
|
|
1696
|
+
parties,
|
|
1595
1697
|
effectiveFrom: handover,
|
|
1596
1698
|
priceSnapshot: priceAt(24.9, 29.63),
|
|
1597
1699
|
lineItems: [lineAt(24.9, 29.63)]
|
|
@@ -1641,13 +1743,17 @@ function persistenceAdapterContract(options) {
|
|
|
1641
1743
|
});
|
|
1642
1744
|
(0, import_node_test.test)("a line keeps the currency and the tax it was booked with", async (t) => {
|
|
1643
1745
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1644
|
-
|
|
1746
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
1747
|
+
if (!contracts || !createSubscriber) {
|
|
1645
1748
|
missing(t, "subscriptionContracts");
|
|
1646
1749
|
return;
|
|
1647
1750
|
}
|
|
1648
1751
|
const tenantId = "tenant-contract-money-facts";
|
|
1752
|
+
const { subscriberId } = await createSubscriber({ legalName: "Z\xFCrich AG" });
|
|
1753
|
+
const parties = { ...partiesWith(subscriberId, "Z\xFCrich AG"), issuer: null };
|
|
1649
1754
|
const created = await contracts.create({
|
|
1650
1755
|
tenantId,
|
|
1756
|
+
parties,
|
|
1651
1757
|
effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
1652
1758
|
priceSnapshot: {
|
|
1653
1759
|
currency: "CHF",
|
|
@@ -1722,9 +1828,9 @@ function persistenceAdapterContract(options) {
|
|
|
1722
1828
|
}
|
|
1723
1829
|
});
|
|
1724
1830
|
(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;
|
|
1831
|
+
const { adapter, seed } = harness;
|
|
1726
1832
|
const contracts = adapter.subscriptionContractRepository;
|
|
1727
|
-
if (!contracts) {
|
|
1833
|
+
if (!contracts || !seed.createSubscriber) {
|
|
1728
1834
|
missing(t, "subscriptionContracts");
|
|
1729
1835
|
return;
|
|
1730
1836
|
}
|
|
@@ -1732,9 +1838,11 @@ function persistenceAdapterContract(options) {
|
|
|
1732
1838
|
t.skip("adapter declares no transaction capability");
|
|
1733
1839
|
return;
|
|
1734
1840
|
}
|
|
1841
|
+
const { subscriberId } = await seed.createSubscriber({ legalName: "Angebot GmbH" });
|
|
1842
|
+
const parties = partiesWith(subscriberId, "Angebot GmbH");
|
|
1735
1843
|
await import_strict.default.rejects(
|
|
1736
1844
|
adapter.transactionRunner.run(async (tx) => {
|
|
1737
|
-
await contracts.create(contractFromOffer("offer-rolled-back"), tx);
|
|
1845
|
+
await contracts.create(contractFromOffer("offer-rolled-back", parties), tx);
|
|
1738
1846
|
throw new Error("the conclusion fails after the contract is written");
|
|
1739
1847
|
})
|
|
1740
1848
|
);
|
|
@@ -1744,13 +1852,817 @@ function persistenceAdapterContract(options) {
|
|
|
1744
1852
|
"the contract outlived its transaction"
|
|
1745
1853
|
);
|
|
1746
1854
|
const kept = await adapter.transactionRunner.run(
|
|
1747
|
-
(tx) => contracts.create(contractFromOffer("offer-kept"), tx)
|
|
1855
|
+
(tx) => contracts.create(contractFromOffer("offer-kept", parties), tx)
|
|
1748
1856
|
);
|
|
1749
1857
|
const found = await contracts.findByOriginalOfferId("offer-kept");
|
|
1750
1858
|
import_strict.default.equal(found?.id, kept.id);
|
|
1751
1859
|
import_strict.default.equal(found?.lineItems.length, 1, "with its lines");
|
|
1752
1860
|
import_strict.default.equal(await contracts.findByOriginalOfferId("offer-nobody-concluded"), null);
|
|
1753
1861
|
});
|
|
1862
|
+
(0, import_node_test.test)("a subscriber is created live for its tenant, and numbered by the database", async (t) => {
|
|
1863
|
+
const subscribers = harness.adapter.subscriberRepository;
|
|
1864
|
+
if (!subscribers) {
|
|
1865
|
+
missing(t, "subscribers");
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
const first = await subscribers.createForTenant({
|
|
1869
|
+
...subscriberFor("tenant-subscriber-first", "Erste Autohaus GmbH"),
|
|
1870
|
+
vatId: "DE123456789",
|
|
1871
|
+
addressLine1: "Hauptstra\xDFe 1",
|
|
1872
|
+
postalCode: "10115",
|
|
1873
|
+
city: "Berlin",
|
|
1874
|
+
country: "DE",
|
|
1875
|
+
invoiceEmail: "rechnung@erste.example",
|
|
1876
|
+
customerNumberPrefix: "K-"
|
|
1877
|
+
});
|
|
1878
|
+
const second = await subscribers.createForTenant(
|
|
1879
|
+
subscriberFor("tenant-subscriber-second", "Zweiter Verein e.V.")
|
|
1880
|
+
);
|
|
1881
|
+
import_strict.default.ok(first && second, "both tenants had no subscriber");
|
|
1882
|
+
const numberOf = (customerNumber) => Number(customerNumber.replace("K-", ""));
|
|
1883
|
+
import_strict.default.ok(first.customerNumber.startsWith("K-"), first.customerNumber);
|
|
1884
|
+
import_strict.default.equal(numberOf(second.customerNumber), numberOf(first.customerNumber) + 1);
|
|
1885
|
+
import_strict.default.ok(numberOf(first.customerNumber) >= 10001, first.customerNumber);
|
|
1886
|
+
const found = await subscribers.findByTenantId("tenant-subscriber-first");
|
|
1887
|
+
import_strict.default.deepEqual(
|
|
1888
|
+
found && {
|
|
1889
|
+
id: found.id,
|
|
1890
|
+
customerNumber: found.customerNumber,
|
|
1891
|
+
tenantId: found.tenantId,
|
|
1892
|
+
legalName: found.legalName,
|
|
1893
|
+
vatId: found.vatId,
|
|
1894
|
+
taxNumber: found.taxNumber,
|
|
1895
|
+
addressLine1: found.addressLine1,
|
|
1896
|
+
addressLine2: found.addressLine2,
|
|
1897
|
+
postalCode: found.postalCode,
|
|
1898
|
+
city: found.city,
|
|
1899
|
+
country: found.country,
|
|
1900
|
+
invoiceEmail: found.invoiceEmail,
|
|
1901
|
+
migrated: found.migrated
|
|
1902
|
+
},
|
|
1903
|
+
{
|
|
1904
|
+
id: first.id,
|
|
1905
|
+
customerNumber: first.customerNumber,
|
|
1906
|
+
tenantId: "tenant-subscriber-first",
|
|
1907
|
+
legalName: "Erste Autohaus GmbH",
|
|
1908
|
+
vatId: "DE123456789",
|
|
1909
|
+
// The nullable half, so an adapter writing '' is caught too.
|
|
1910
|
+
taxNumber: null,
|
|
1911
|
+
addressLine1: "Hauptstra\xDFe 1",
|
|
1912
|
+
addressLine2: null,
|
|
1913
|
+
postalCode: "10115",
|
|
1914
|
+
city: "Berlin",
|
|
1915
|
+
country: "DE",
|
|
1916
|
+
invoiceEmail: "rechnung@erste.example",
|
|
1917
|
+
migrated: false
|
|
1918
|
+
}
|
|
1919
|
+
);
|
|
1920
|
+
import_strict.default.equal(
|
|
1921
|
+
(await subscribers.findById(second.id))?.tenantId,
|
|
1922
|
+
"tenant-subscriber-second"
|
|
1923
|
+
);
|
|
1924
|
+
import_strict.default.equal(await subscribers.findByTenantId("tenant-without-subscriber"), null);
|
|
1925
|
+
import_strict.default.equal(await subscribers.findById("subscriber-nobody-created"), null);
|
|
1926
|
+
});
|
|
1927
|
+
(0, import_node_test.test)("a tenant has one live subscriber: a second is refused, and the transaction survives it", async (t) => {
|
|
1928
|
+
const { adapter } = harness;
|
|
1929
|
+
const subscribers = adapter.subscriberRepository;
|
|
1930
|
+
if (!subscribers) {
|
|
1931
|
+
missing(t, "subscribers");
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
if (!adapter.capabilities.transactions) {
|
|
1935
|
+
t.skip("adapter declares no transaction capability");
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
const first = await subscribers.createForTenant(
|
|
1939
|
+
subscriberFor("tenant-one-subscriber", "Einzig GmbH")
|
|
1940
|
+
);
|
|
1941
|
+
const afterRefusal = await adapter.transactionRunner.run(async (tx) => {
|
|
1942
|
+
const refused = await subscribers.createForTenant(
|
|
1943
|
+
subscriberFor("tenant-one-subscriber", "Doppelt GmbH"),
|
|
1944
|
+
tx
|
|
1945
|
+
);
|
|
1946
|
+
import_strict.default.equal(refused, null, "a second live subscriber was created");
|
|
1947
|
+
return subscribers.createForTenant(
|
|
1948
|
+
subscriberFor("tenant-after-refusal", "Danach GmbH"),
|
|
1949
|
+
tx
|
|
1950
|
+
);
|
|
1951
|
+
});
|
|
1952
|
+
import_strict.default.equal(
|
|
1953
|
+
(await subscribers.findByTenantId("tenant-one-subscriber"))?.id,
|
|
1954
|
+
first?.id
|
|
1955
|
+
);
|
|
1956
|
+
import_strict.default.equal(
|
|
1957
|
+
(await subscribers.findByTenantId("tenant-after-refusal"))?.id,
|
|
1958
|
+
afterRefusal?.id,
|
|
1959
|
+
"the transaction did not survive the refusal"
|
|
1960
|
+
);
|
|
1961
|
+
});
|
|
1962
|
+
(0, import_node_test.test)("creating a subscriber for one tenant twice at once ends with one", async (t) => {
|
|
1963
|
+
const subscribers = harness.adapter.subscriberRepository;
|
|
1964
|
+
if (!subscribers) {
|
|
1965
|
+
missing(t, "subscribers");
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
const results = await Promise.all([
|
|
1969
|
+
subscribers.createForTenant(subscriberFor("tenant-at-once", "Gleichzeitig A")),
|
|
1970
|
+
subscribers.createForTenant(subscriberFor("tenant-at-once", "Gleichzeitig B"))
|
|
1971
|
+
]);
|
|
1972
|
+
const created = results.filter((result) => result !== null);
|
|
1973
|
+
import_strict.default.equal(created.length, 1, `${created.length} subscribers were created`);
|
|
1974
|
+
import_strict.default.equal((await subscribers.findByTenantId("tenant-at-once"))?.id, created[0].id);
|
|
1975
|
+
});
|
|
1976
|
+
(0, import_node_test.test)("a subscriber written on a transaction is undone with it", async (t) => {
|
|
1977
|
+
const { adapter } = harness;
|
|
1978
|
+
const subscribers = adapter.subscriberRepository;
|
|
1979
|
+
if (!subscribers) {
|
|
1980
|
+
missing(t, "subscribers");
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
if (!adapter.capabilities.transactions) {
|
|
1984
|
+
t.skip("adapter declares no transaction capability");
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
await import_strict.default.rejects(
|
|
1988
|
+
adapter.transactionRunner.run(async (tx) => {
|
|
1989
|
+
await subscribers.createForTenant(
|
|
1990
|
+
subscriberFor("tenant-rolled-back", "Zur\xFCckgerollt GmbH"),
|
|
1991
|
+
tx
|
|
1992
|
+
);
|
|
1993
|
+
throw new Error("the tenant is not created after all");
|
|
1994
|
+
})
|
|
1995
|
+
);
|
|
1996
|
+
import_strict.default.equal(await subscribers.findByTenantId("tenant-rolled-back"), null);
|
|
1997
|
+
import_strict.default.ok(
|
|
1998
|
+
await subscribers.createForTenant(
|
|
1999
|
+
subscriberFor("tenant-rolled-back", "Zur\xFCckgerollt GmbH")
|
|
2000
|
+
)
|
|
2001
|
+
);
|
|
2002
|
+
});
|
|
2003
|
+
(0, import_node_test.test)("a contact change writes what it names and keeps the rest", async (t) => {
|
|
2004
|
+
const subscribers = harness.adapter.subscriberRepository;
|
|
2005
|
+
if (!subscribers) {
|
|
2006
|
+
missing(t, "subscribers");
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
const created = await subscribers.createForTenant({
|
|
2010
|
+
...subscriberFor("tenant-contact", "Kontakt GmbH"),
|
|
2011
|
+
addressLine2: "Hinterhaus",
|
|
2012
|
+
city: "Hamburg"
|
|
2013
|
+
});
|
|
2014
|
+
import_strict.default.ok(created);
|
|
2015
|
+
const changed = await subscribers.updateContact(created.id, {
|
|
2016
|
+
city: "Bremen",
|
|
2017
|
+
addressLine2: null,
|
|
2018
|
+
invoiceEmail: "buchhaltung@kontakt.example"
|
|
2019
|
+
});
|
|
2020
|
+
import_strict.default.deepEqual(
|
|
2021
|
+
changed && [
|
|
2022
|
+
changed.city,
|
|
2023
|
+
changed.addressLine2,
|
|
2024
|
+
changed.invoiceEmail,
|
|
2025
|
+
changed.legalName,
|
|
2026
|
+
changed.tenantId
|
|
2027
|
+
],
|
|
2028
|
+
["Bremen", null, "buchhaltung@kontakt.example", "Kontakt GmbH", "tenant-contact"]
|
|
2029
|
+
);
|
|
2030
|
+
import_strict.default.equal((await subscribers.findById(created.id))?.city, "Bremen");
|
|
2031
|
+
import_strict.default.equal(
|
|
2032
|
+
await subscribers.updateContact("subscriber-nobody-created", { city: "Kiel" }),
|
|
2033
|
+
null
|
|
2034
|
+
);
|
|
2035
|
+
});
|
|
2036
|
+
(0, import_node_test.test)("a correction records the values it replaced, and nothing when nothing moves", async (t) => {
|
|
2037
|
+
const subscribers = harness.adapter.subscriberRepository;
|
|
2038
|
+
if (!subscribers) {
|
|
2039
|
+
missing(t, "subscribers");
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const created = await subscribers.createForTenant(
|
|
2043
|
+
subscriberFor("tenant-correction", "Mueller GmbH")
|
|
2044
|
+
);
|
|
2045
|
+
import_strict.default.ok(created);
|
|
2046
|
+
const at = (day) => new Date(Date.UTC(2026, 8, day));
|
|
2047
|
+
const first = await subscribers.correctIdentity(created.id, {
|
|
2048
|
+
corrected: { legalName: "M\xFCller GmbH", vatId: "DE123456789", taxNumber: null },
|
|
2049
|
+
reason: "Umlaut lost when the registration was typed",
|
|
2050
|
+
correctedBy: "operator:anna",
|
|
2051
|
+
correctedAt: at(1)
|
|
2052
|
+
});
|
|
2053
|
+
import_strict.default.deepEqual(
|
|
2054
|
+
first?.correction && {
|
|
2055
|
+
previous: first.correction.previous,
|
|
2056
|
+
corrected: first.correction.corrected,
|
|
2057
|
+
reason: first.correction.reason,
|
|
2058
|
+
correctedBy: first.correction.correctedBy,
|
|
2059
|
+
correctedAt: first.correction.correctedAt.getTime()
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
previous: { legalName: "Mueller GmbH", vatId: null },
|
|
2063
|
+
corrected: { legalName: "M\xFCller GmbH", vatId: "DE123456789" },
|
|
2064
|
+
reason: "Umlaut lost when the registration was typed",
|
|
2065
|
+
correctedBy: "operator:anna",
|
|
2066
|
+
correctedAt: at(1).getTime()
|
|
2067
|
+
}
|
|
2068
|
+
);
|
|
2069
|
+
import_strict.default.equal(first?.subscriber.legalName, "M\xFCller GmbH");
|
|
2070
|
+
import_strict.default.equal((await subscribers.findById(created.id))?.vatId, "DE123456789");
|
|
2071
|
+
const unchanged = await subscribers.correctIdentity(created.id, {
|
|
2072
|
+
corrected: { legalName: "M\xFCller GmbH" },
|
|
2073
|
+
reason: "Clicked twice",
|
|
2074
|
+
correctedBy: "operator:anna",
|
|
2075
|
+
correctedAt: at(2)
|
|
2076
|
+
});
|
|
2077
|
+
import_strict.default.equal(
|
|
2078
|
+
unchanged?.correction,
|
|
2079
|
+
null,
|
|
2080
|
+
"a correction that moved nothing was recorded"
|
|
2081
|
+
);
|
|
2082
|
+
await subscribers.correctIdentity(created.id, {
|
|
2083
|
+
corrected: { vatId: "DE999999999" },
|
|
2084
|
+
reason: "Wrong VAT id on the first correction",
|
|
2085
|
+
correctedBy: "operator:ben",
|
|
2086
|
+
correctedAt: at(3)
|
|
2087
|
+
});
|
|
2088
|
+
const listed = await subscribers.listCorrections(created.id);
|
|
2089
|
+
import_strict.default.deepEqual(
|
|
2090
|
+
listed.map((correction) => correction.reason),
|
|
2091
|
+
[
|
|
2092
|
+
"Wrong VAT id on the first correction",
|
|
2093
|
+
"Umlaut lost when the registration was typed"
|
|
2094
|
+
],
|
|
2095
|
+
"corrections come back the latest first, and only the two that moved something"
|
|
2096
|
+
);
|
|
2097
|
+
import_strict.default.equal(
|
|
2098
|
+
await subscribers.correctIdentity("subscriber-nobody-created", {
|
|
2099
|
+
corrected: { legalName: "Niemand" },
|
|
2100
|
+
reason: "none",
|
|
2101
|
+
correctedBy: "operator:anna",
|
|
2102
|
+
correctedAt: at(4)
|
|
2103
|
+
}),
|
|
2104
|
+
null
|
|
2105
|
+
);
|
|
2106
|
+
});
|
|
2107
|
+
(0, import_node_test.test)("two corrections at once each record the value the other left behind", async (t) => {
|
|
2108
|
+
const { adapter } = harness;
|
|
2109
|
+
const subscribers = adapter.subscriberRepository;
|
|
2110
|
+
if (!subscribers) {
|
|
2111
|
+
missing(t, "subscribers");
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (!adapter.capabilities.transactions || !adapter.capabilities.pessimisticLocking) {
|
|
2115
|
+
t.skip("adapter declares no transactions or no row locks");
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
const created = await subscribers.createForTenant(
|
|
2119
|
+
subscriberFor("tenant-corrected-twice", "Original GmbH")
|
|
2120
|
+
);
|
|
2121
|
+
import_strict.default.ok(created);
|
|
2122
|
+
const correctTo = (legalName) => adapter.transactionRunner.run(async (tx) => {
|
|
2123
|
+
await subscribers.correctIdentity(
|
|
2124
|
+
created.id,
|
|
2125
|
+
{
|
|
2126
|
+
corrected: { legalName },
|
|
2127
|
+
reason: `to ${legalName}`,
|
|
2128
|
+
correctedBy: "operator:anna",
|
|
2129
|
+
correctedAt: /* @__PURE__ */ new Date()
|
|
2130
|
+
},
|
|
2131
|
+
tx
|
|
2132
|
+
);
|
|
2133
|
+
await sleep(LOCK_HOLD_MS);
|
|
2134
|
+
});
|
|
2135
|
+
await Promise.all([
|
|
2136
|
+
correctTo("Erste Korrektur GmbH"),
|
|
2137
|
+
correctTo("Zweite Korrektur GmbH")
|
|
2138
|
+
]);
|
|
2139
|
+
const listed = await subscribers.listCorrections(created.id);
|
|
2140
|
+
import_strict.default.equal(listed.length, 2);
|
|
2141
|
+
const replaced = listed.map((correction) => correction.previous.legalName).sort();
|
|
2142
|
+
const written = listed.map((correction) => correction.corrected.legalName);
|
|
2143
|
+
const current = (await subscribers.findById(created.id))?.legalName;
|
|
2144
|
+
import_strict.default.ok(replaced.includes("Original GmbH"), JSON.stringify(listed));
|
|
2145
|
+
const secondReplaced = replaced.find((name) => name !== "Original GmbH");
|
|
2146
|
+
import_strict.default.ok(
|
|
2147
|
+
secondReplaced !== void 0 && written.includes(secondReplaced),
|
|
2148
|
+
`a correction recorded a value it did not replace: ${JSON.stringify(listed)}`
|
|
2149
|
+
);
|
|
2150
|
+
import_strict.default.ok(current !== void 0 && written.includes(current));
|
|
2151
|
+
import_strict.default.notEqual(secondReplaced, current);
|
|
2152
|
+
});
|
|
2153
|
+
(0, import_node_test.test)("a contract keeps its subscriber's copy after the subscriber is corrected", async (t) => {
|
|
2154
|
+
const { adapter } = harness;
|
|
2155
|
+
const subscribers = adapter.subscriberRepository;
|
|
2156
|
+
const contracts = adapter.subscriptionContractRepository;
|
|
2157
|
+
if (!subscribers) {
|
|
2158
|
+
missing(t, "subscribers");
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
if (!contracts || !harness.seed.createSubscriber) {
|
|
2162
|
+
missing(t, "subscriptionContracts");
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
const subscriber = await subscribers.createForTenant(
|
|
2166
|
+
subscriberFor("tenant-copy-kept", "Vorher GmbH")
|
|
2167
|
+
);
|
|
2168
|
+
import_strict.default.ok(subscriber);
|
|
2169
|
+
const parties = partiesWith(subscriber.id, "Vorher GmbH");
|
|
2170
|
+
const contract = await contracts.create({
|
|
2171
|
+
...contractFromOffer("offer-copy-kept", parties),
|
|
2172
|
+
tenantId: "tenant-copy-kept"
|
|
2173
|
+
});
|
|
2174
|
+
await subscribers.correctIdentity(subscriber.id, {
|
|
2175
|
+
corrected: { legalName: "Nachher GmbH" },
|
|
2176
|
+
reason: "Change of name of the same company",
|
|
2177
|
+
correctedBy: "operator:anna",
|
|
2178
|
+
correctedAt: /* @__PURE__ */ new Date()
|
|
2179
|
+
});
|
|
2180
|
+
const readBack = await contracts.findById(contract.id);
|
|
2181
|
+
import_strict.default.equal(readBack?.subscriberId, subscriber.id);
|
|
2182
|
+
import_strict.default.equal(
|
|
2183
|
+
readBack?.subscriber.legalName,
|
|
2184
|
+
"Vorher GmbH",
|
|
2185
|
+
"the contract followed a correction of the live record"
|
|
2186
|
+
);
|
|
2187
|
+
});
|
|
2188
|
+
(0, import_node_test.test)("a gateway event is claimed once per account, and a duplicate leaves the transaction usable", async (t) => {
|
|
2189
|
+
const { adapter } = harness;
|
|
2190
|
+
const log = adapter.paymentEventLog;
|
|
2191
|
+
if (!log) {
|
|
2192
|
+
missing(t, "paymentEventLog");
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
const claims = await adapter.transactionRunner.run(async (tx) => [
|
|
2196
|
+
await log.claim(eventAt("stripe-main", "evt_1"), tx),
|
|
2197
|
+
await log.claim(eventAt("stripe-main", "evt_1"), tx),
|
|
2198
|
+
// The same identifier from another account is another event.
|
|
2199
|
+
await log.claim(eventAt("stripe-old", "evt_1"), tx),
|
|
2200
|
+
// A duplicate that raised would have aborted the transaction here.
|
|
2201
|
+
await log.claim(eventAt("stripe-main", "evt_2"), tx)
|
|
2202
|
+
]);
|
|
2203
|
+
import_strict.default.deepEqual(claims, [true, false, true, true]);
|
|
2204
|
+
const later = await adapter.transactionRunner.run(
|
|
2205
|
+
(tx) => log.claim(eventAt("stripe-main", "evt_1"), tx)
|
|
2206
|
+
);
|
|
2207
|
+
import_strict.default.equal(later, false, "a committed claim was claimed again");
|
|
2208
|
+
});
|
|
2209
|
+
(0, import_node_test.test)("one gateway session is confirmed once, however many events report it", async (t) => {
|
|
2210
|
+
const { adapter } = harness;
|
|
2211
|
+
const log = adapter.paymentEventLog;
|
|
2212
|
+
if (!log) {
|
|
2213
|
+
missing(t, "paymentEventLog");
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const session = "cs_reported_twice";
|
|
2217
|
+
const claims = await adapter.transactionRunner.run(async (tx) => [
|
|
2218
|
+
await log.claim(
|
|
2219
|
+
eventAt("stripe-main", "evt_form_done", { sessionId: session }),
|
|
2220
|
+
tx
|
|
2221
|
+
),
|
|
2222
|
+
// The same session, reported again under another identifier:
|
|
2223
|
+
// recording it would set the session's payment method up twice.
|
|
2224
|
+
await log.claim(
|
|
2225
|
+
eventAt("stripe-main", "evt_method_on", { sessionId: session }),
|
|
2226
|
+
tx
|
|
2227
|
+
),
|
|
2228
|
+
// Another account's session of that name is another session.
|
|
2229
|
+
await log.claim(eventAt("stripe-old", "evt_elsewhere", { sessionId: session }), tx),
|
|
2230
|
+
// A kind that says nothing about the session being confirmed.
|
|
2231
|
+
await log.claim(
|
|
2232
|
+
eventAt("stripe-main", "evt_gave_up", {
|
|
2233
|
+
sessionId: session,
|
|
2234
|
+
kind: "payment-method-setup-failed"
|
|
2235
|
+
}),
|
|
2236
|
+
tx
|
|
2237
|
+
),
|
|
2238
|
+
// Events about no session at all do not collide with each other.
|
|
2239
|
+
await log.claim(
|
|
2240
|
+
eventAt("stripe-main", "evt_other_1", { sessionId: null, kind: "unhandled" }),
|
|
2241
|
+
tx
|
|
2242
|
+
),
|
|
2243
|
+
await log.claim(
|
|
2244
|
+
eventAt("stripe-main", "evt_other_2", { sessionId: null, kind: "unhandled" }),
|
|
2245
|
+
tx
|
|
2246
|
+
)
|
|
2247
|
+
]);
|
|
2248
|
+
import_strict.default.deepEqual(claims, [true, false, true, true, true, true]);
|
|
2249
|
+
const later = await adapter.transactionRunner.run(
|
|
2250
|
+
(tx) => log.claim(eventAt("stripe-main", "evt_late", { sessionId: session }), tx)
|
|
2251
|
+
);
|
|
2252
|
+
import_strict.default.equal(later, false, "a session already confirmed was confirmed again");
|
|
2253
|
+
});
|
|
2254
|
+
(0, import_node_test.test)("a claim rolled back with its transaction is free for the retry", async (t) => {
|
|
2255
|
+
const { adapter } = harness;
|
|
2256
|
+
const log = adapter.paymentEventLog;
|
|
2257
|
+
if (!log) {
|
|
2258
|
+
missing(t, "paymentEventLog");
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
await import_strict.default.rejects(
|
|
2262
|
+
adapter.transactionRunner.run(async (tx) => {
|
|
2263
|
+
import_strict.default.equal(await log.claim(eventAt("stripe-main", "evt_retry"), tx), true);
|
|
2264
|
+
throw new Error("recording what the event changes failed");
|
|
2265
|
+
}),
|
|
2266
|
+
/recording what the event changes failed/
|
|
2267
|
+
);
|
|
2268
|
+
const retry = await adapter.transactionRunner.run(
|
|
2269
|
+
(tx) => log.claim(eventAt("stripe-main", "evt_retry"), tx)
|
|
2270
|
+
);
|
|
2271
|
+
import_strict.default.equal(retry, true, "the retry was discarded as a duplicate");
|
|
2272
|
+
});
|
|
2273
|
+
(0, import_node_test.test)("a delivery that meets a claim still open waits for it, and answers by its outcome", async (t) => {
|
|
2274
|
+
const { adapter } = harness;
|
|
2275
|
+
const log = adapter.paymentEventLog;
|
|
2276
|
+
if (!log) {
|
|
2277
|
+
missing(t, "paymentEventLog");
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (!adapter.capabilities.pessimisticLocking) {
|
|
2281
|
+
t.skip(
|
|
2282
|
+
"adapter declares no pessimistic locking: an open claim cannot be waited on"
|
|
2283
|
+
);
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
const [committed, afterCommit] = await Promise.all([
|
|
2287
|
+
adapter.transactionRunner.run(async (tx) => {
|
|
2288
|
+
const claimed = await log.claim(eventAt("stripe-main", "evt_race"), tx);
|
|
2289
|
+
await sleep(LOCK_HOLD_MS);
|
|
2290
|
+
return claimed;
|
|
2291
|
+
}),
|
|
2292
|
+
sleep(LOCK_HOLD_MS / 3).then(
|
|
2293
|
+
() => adapter.transactionRunner.run(
|
|
2294
|
+
(tx) => log.claim(eventAt("stripe-main", "evt_race"), tx)
|
|
2295
|
+
)
|
|
2296
|
+
)
|
|
2297
|
+
]);
|
|
2298
|
+
import_strict.default.deepEqual([committed, afterCommit], [true, false]);
|
|
2299
|
+
const [rolledBack, afterRollback] = await Promise.allSettled([
|
|
2300
|
+
adapter.transactionRunner.run(async (tx) => {
|
|
2301
|
+
await log.claim(eventAt("stripe-main", "evt_race_back"), tx);
|
|
2302
|
+
await sleep(LOCK_HOLD_MS);
|
|
2303
|
+
throw new Error("the first delivery failed");
|
|
2304
|
+
}),
|
|
2305
|
+
sleep(LOCK_HOLD_MS / 3).then(
|
|
2306
|
+
() => adapter.transactionRunner.run(
|
|
2307
|
+
(tx) => log.claim(eventAt("stripe-main", "evt_race_back"), tx)
|
|
2308
|
+
)
|
|
2309
|
+
)
|
|
2310
|
+
]);
|
|
2311
|
+
import_strict.default.equal(rolledBack.status, "rejected");
|
|
2312
|
+
import_strict.default.deepEqual(afterRollback, { status: "fulfilled", value: true });
|
|
2313
|
+
});
|
|
2314
|
+
(0, import_node_test.test)("an event that changed nothing gives its session back, and stays claimed itself", async (t) => {
|
|
2315
|
+
const { adapter } = harness;
|
|
2316
|
+
const log = adapter.paymentEventLog;
|
|
2317
|
+
if (!log) {
|
|
2318
|
+
missing(t, "paymentEventLog");
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
const session = "cs_nothing_to_do";
|
|
2322
|
+
const released = await adapter.transactionRunner.run(async (tx) => {
|
|
2323
|
+
await log.claim(eventAt("stripe-main", "evt_missed", { sessionId: session }), tx);
|
|
2324
|
+
await log.releaseSession("stripe-main", "evt_missed", tx);
|
|
2325
|
+
return [
|
|
2326
|
+
// The next event about that session is handled …
|
|
2327
|
+
await log.claim(
|
|
2328
|
+
eventAt("stripe-main", "evt_correct", { sessionId: session }),
|
|
2329
|
+
tx
|
|
2330
|
+
),
|
|
2331
|
+
// … while the event that released it stays claimed.
|
|
2332
|
+
await log.claim(eventAt("stripe-main", "evt_missed", { sessionId: null }), tx)
|
|
2333
|
+
];
|
|
2334
|
+
});
|
|
2335
|
+
import_strict.default.deepEqual(released, [true, false]);
|
|
2336
|
+
const again = await adapter.transactionRunner.run(
|
|
2337
|
+
(tx) => log.claim(eventAt("stripe-main", "evt_after_correct", { sessionId: session }), tx)
|
|
2338
|
+
);
|
|
2339
|
+
import_strict.default.equal(again, false, "the session was confirmed and is not free again");
|
|
2340
|
+
});
|
|
2341
|
+
(0, import_node_test.test)("two events confirming one session at once end with one claim", async (t) => {
|
|
2342
|
+
const { adapter } = harness;
|
|
2343
|
+
const log = adapter.paymentEventLog;
|
|
2344
|
+
if (!log) {
|
|
2345
|
+
missing(t, "paymentEventLog");
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
if (!adapter.capabilities.pessimisticLocking) {
|
|
2349
|
+
t.skip(
|
|
2350
|
+
"adapter declares no pessimistic locking: an open claim cannot be waited on"
|
|
2351
|
+
);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
const session = "cs_at_once";
|
|
2355
|
+
const [first, second] = await Promise.all([
|
|
2356
|
+
adapter.transactionRunner.run(async (tx) => {
|
|
2357
|
+
const claimed = await log.claim(
|
|
2358
|
+
eventAt("stripe-main", "evt_at_once_a", { sessionId: session }),
|
|
2359
|
+
tx
|
|
2360
|
+
);
|
|
2361
|
+
await sleep(LOCK_HOLD_MS);
|
|
2362
|
+
return claimed;
|
|
2363
|
+
}),
|
|
2364
|
+
sleep(LOCK_HOLD_MS / 3).then(
|
|
2365
|
+
() => adapter.transactionRunner.run(
|
|
2366
|
+
(tx) => log.claim(
|
|
2367
|
+
eventAt("stripe-main", "evt_at_once_b", { sessionId: session }),
|
|
2368
|
+
tx
|
|
2369
|
+
)
|
|
2370
|
+
)
|
|
2371
|
+
)
|
|
2372
|
+
]);
|
|
2373
|
+
import_strict.default.deepEqual([first, second], [true, false]);
|
|
2374
|
+
});
|
|
2375
|
+
(0, import_node_test.test)("a confirmed payment method becomes the subscriber's, with its references and masked details", async (t) => {
|
|
2376
|
+
const methods = harness.adapter.subscriberPaymentMethodRepository;
|
|
2377
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2378
|
+
if (!methods || !createSubscriber) {
|
|
2379
|
+
missing(t, "subscriberPaymentMethods");
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
const { subscriberId } = await createSubscriber({ legalName: "Karte GmbH" });
|
|
2383
|
+
const debit = {
|
|
2384
|
+
...paymentMethodFor(subscriberId, "pm_sepa", "2026-09-01T10:00:00.000Z"),
|
|
2385
|
+
type: "sepa_debit",
|
|
2386
|
+
brand: null,
|
|
2387
|
+
last4: "3000",
|
|
2388
|
+
expiryMonth: null,
|
|
2389
|
+
expiryYear: null,
|
|
2390
|
+
country: "DE",
|
|
2391
|
+
bankCode: "37040044",
|
|
2392
|
+
mandateReference: "MANDATE-1"
|
|
2393
|
+
};
|
|
2394
|
+
const result = await methods.recordConfirmed(debit);
|
|
2395
|
+
import_strict.default.equal(result.outcome, "activated");
|
|
2396
|
+
const { id, createdAt, ...stored } = result.method;
|
|
2397
|
+
import_strict.default.ok(id);
|
|
2398
|
+
import_strict.default.ok(createdAt instanceof Date);
|
|
2399
|
+
import_strict.default.deepEqual(stored, { ...debit, status: "ACTIVE", replacedAt: null });
|
|
2400
|
+
import_strict.default.deepEqual(await methods.findActive(subscriberId), result.method);
|
|
2401
|
+
import_strict.default.deepEqual(
|
|
2402
|
+
await methods.findByReference("stripe-main", "pm_sepa"),
|
|
2403
|
+
result.method
|
|
2404
|
+
);
|
|
2405
|
+
import_strict.default.equal(await methods.findByReference("stripe-old", "pm_sepa"), null);
|
|
2406
|
+
const other = await createSubscriber({ legalName: "Second Customer GmbH" });
|
|
2407
|
+
import_strict.default.equal(await methods.findActive(other.subscriberId), null);
|
|
2408
|
+
});
|
|
2409
|
+
(0, import_node_test.test)("a newer payment method takes over, and the one it replaced stays as history", async (t) => {
|
|
2410
|
+
const methods = harness.adapter.subscriberPaymentMethodRepository;
|
|
2411
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2412
|
+
if (!methods || !createSubscriber) {
|
|
2413
|
+
missing(t, "subscriberPaymentMethods");
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
const { subscriberId } = await createSubscriber({ legalName: "Wechsel GmbH" });
|
|
2417
|
+
await methods.recordConfirmed(
|
|
2418
|
+
paymentMethodFor(subscriberId, "pm_first", "2026-09-01T10:00:00.000Z")
|
|
2419
|
+
);
|
|
2420
|
+
const second = await methods.recordConfirmed(
|
|
2421
|
+
paymentMethodFor(subscriberId, "pm_second", "2026-09-02T10:00:00.000Z")
|
|
2422
|
+
);
|
|
2423
|
+
import_strict.default.equal(second.outcome, "activated");
|
|
2424
|
+
import_strict.default.equal((await methods.findActive(subscriberId))?.paymentMethodRef, "pm_second");
|
|
2425
|
+
const first = await methods.findByReference("stripe-main", "pm_first");
|
|
2426
|
+
import_strict.default.equal(first?.status, "REPLACED");
|
|
2427
|
+
import_strict.default.equal(first?.replacedAt?.toISOString(), "2026-09-02T10:00:00.000Z");
|
|
2428
|
+
});
|
|
2429
|
+
(0, import_node_test.test)("a confirmation recorded again is recognised, and changes nothing", async (t) => {
|
|
2430
|
+
const methods = harness.adapter.subscriberPaymentMethodRepository;
|
|
2431
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2432
|
+
if (!methods || !createSubscriber) {
|
|
2433
|
+
missing(t, "subscriberPaymentMethods");
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const { subscriberId } = await createSubscriber({ legalName: "Doppelt GmbH" });
|
|
2437
|
+
const confirmation = paymentMethodFor(
|
|
2438
|
+
subscriberId,
|
|
2439
|
+
"pm_twice",
|
|
2440
|
+
"2026-09-01T10:00:00.000Z"
|
|
2441
|
+
);
|
|
2442
|
+
const first = await methods.recordConfirmed(confirmation);
|
|
2443
|
+
const again = await methods.recordConfirmed({
|
|
2444
|
+
...confirmation,
|
|
2445
|
+
confirmedAt: /* @__PURE__ */ new Date("2026-09-03T10:00:00.000Z")
|
|
2446
|
+
});
|
|
2447
|
+
import_strict.default.equal(again.outcome, "already-recorded");
|
|
2448
|
+
import_strict.default.deepEqual(again.method, first.method);
|
|
2449
|
+
import_strict.default.deepEqual(await methods.findActive(subscriberId), first.method);
|
|
2450
|
+
});
|
|
2451
|
+
(0, import_node_test.test)("a confirmation older than the payment method in use is recorded as already replaced", async (t) => {
|
|
2452
|
+
const methods = harness.adapter.subscriberPaymentMethodRepository;
|
|
2453
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2454
|
+
if (!methods || !createSubscriber) {
|
|
2455
|
+
missing(t, "subscriberPaymentMethods");
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
const { subscriberId } = await createSubscriber({ legalName: "Reihenfolge GmbH" });
|
|
2459
|
+
await methods.recordConfirmed(
|
|
2460
|
+
paymentMethodFor(subscriberId, "pm_later", "2026-09-02T10:00:00.000Z")
|
|
2461
|
+
);
|
|
2462
|
+
const earlier = await methods.recordConfirmed(
|
|
2463
|
+
paymentMethodFor(subscriberId, "pm_earlier", "2026-09-01T10:00:00.000Z")
|
|
2464
|
+
);
|
|
2465
|
+
import_strict.default.equal(earlier.outcome, "superseded");
|
|
2466
|
+
import_strict.default.equal(earlier.method.status, "REPLACED");
|
|
2467
|
+
import_strict.default.equal(earlier.method.replacedAt?.toISOString(), "2026-09-02T10:00:00.000Z");
|
|
2468
|
+
import_strict.default.equal((await methods.findActive(subscriberId))?.paymentMethodRef, "pm_later");
|
|
2469
|
+
});
|
|
2470
|
+
(0, import_node_test.test)("two confirmations for one subscriber at once leave one payment method in use", async (t) => {
|
|
2471
|
+
const methods = harness.adapter.subscriberPaymentMethodRepository;
|
|
2472
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2473
|
+
if (!methods || !createSubscriber) {
|
|
2474
|
+
missing(t, "subscriberPaymentMethods");
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
const { subscriberId } = await createSubscriber({ legalName: "Gleichzeitig GmbH" });
|
|
2478
|
+
const results = await Promise.all([
|
|
2479
|
+
methods.recordConfirmed(
|
|
2480
|
+
paymentMethodFor(subscriberId, "pm_at_once_a", "2026-09-01T10:00:00.000Z")
|
|
2481
|
+
),
|
|
2482
|
+
methods.recordConfirmed(
|
|
2483
|
+
paymentMethodFor(subscriberId, "pm_at_once_b", "2026-09-01T10:00:01.000Z")
|
|
2484
|
+
)
|
|
2485
|
+
]);
|
|
2486
|
+
for (const result of results) {
|
|
2487
|
+
import_strict.default.ok(
|
|
2488
|
+
result.outcome === "activated" || result.outcome === "superseded",
|
|
2489
|
+
result.outcome
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
const statuses = await Promise.all(
|
|
2493
|
+
["pm_at_once_a", "pm_at_once_b"].map(
|
|
2494
|
+
async (ref) => (await methods.findByReference("stripe-main", ref))?.status
|
|
2495
|
+
)
|
|
2496
|
+
);
|
|
2497
|
+
import_strict.default.deepEqual(statuses, ["REPLACED", "ACTIVE"]);
|
|
2498
|
+
import_strict.default.equal(
|
|
2499
|
+
(await methods.findActive(subscriberId))?.paymentMethodRef,
|
|
2500
|
+
"pm_at_once_b"
|
|
2501
|
+
);
|
|
2502
|
+
});
|
|
2503
|
+
(0, import_node_test.test)("a payment method written on a transaction is undone with it", async (t) => {
|
|
2504
|
+
const { adapter } = harness;
|
|
2505
|
+
const methods = adapter.subscriberPaymentMethodRepository;
|
|
2506
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2507
|
+
if (!methods || !createSubscriber) {
|
|
2508
|
+
missing(t, "subscriberPaymentMethods");
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
const { subscriberId } = await createSubscriber({ legalName: "Rolled Back GmbH" });
|
|
2512
|
+
await import_strict.default.rejects(
|
|
2513
|
+
adapter.transactionRunner.run(async (tx) => {
|
|
2514
|
+
await methods.recordConfirmed(
|
|
2515
|
+
paymentMethodFor(
|
|
2516
|
+
subscriberId,
|
|
2517
|
+
"pm_rolled_back",
|
|
2518
|
+
"2026-09-01T10:00:00.000Z"
|
|
2519
|
+
),
|
|
2520
|
+
tx
|
|
2521
|
+
);
|
|
2522
|
+
throw new Error("the activation failed after all");
|
|
2523
|
+
}),
|
|
2524
|
+
/the activation failed after all/
|
|
2525
|
+
);
|
|
2526
|
+
import_strict.default.equal(await methods.findActive(subscriberId), null);
|
|
2527
|
+
import_strict.default.equal(await methods.findByReference("stripe-main", "pm_rolled_back"), null);
|
|
2528
|
+
});
|
|
2529
|
+
(0, import_node_test.test)("a payment method for a subscriber that does not exist is refused", async (t) => {
|
|
2530
|
+
const methods = harness.adapter.subscriberPaymentMethodRepository;
|
|
2531
|
+
if (!methods || !harness.seed.createSubscriber) {
|
|
2532
|
+
missing(t, "subscriberPaymentMethods");
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
await import_strict.default.rejects(
|
|
2536
|
+
methods.recordConfirmed(
|
|
2537
|
+
paymentMethodFor(
|
|
2538
|
+
"subscriber-nobody-created",
|
|
2539
|
+
"pm_nobody",
|
|
2540
|
+
"2026-09-01T10:00:00.000Z"
|
|
2541
|
+
)
|
|
2542
|
+
)
|
|
2543
|
+
);
|
|
2544
|
+
import_strict.default.equal(await methods.findByReference("stripe-main", "pm_nobody"), null);
|
|
2545
|
+
});
|
|
2546
|
+
(0, import_node_test.test)("the accounts in use are those holding a payment method in use, each once", async (t) => {
|
|
2547
|
+
const methods = harness.adapter.subscriberPaymentMethodRepository;
|
|
2548
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2549
|
+
if (!methods || !createSubscriber) {
|
|
2550
|
+
missing(t, "subscriberPaymentMethods");
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
import_strict.default.deepEqual(await methods.accountsInUse(), []);
|
|
2554
|
+
const moved = await createSubscriber({ legalName: "Umgezogen GmbH" });
|
|
2555
|
+
const stayed = await createSubscriber({ legalName: "Geblieben GmbH" });
|
|
2556
|
+
await methods.recordConfirmed(
|
|
2557
|
+
paymentMethodFor(
|
|
2558
|
+
moved.subscriberId,
|
|
2559
|
+
"pm_old_account",
|
|
2560
|
+
"2026-09-01T10:00:00.000Z",
|
|
2561
|
+
"stripe-old"
|
|
2562
|
+
)
|
|
2563
|
+
);
|
|
2564
|
+
await methods.recordConfirmed(
|
|
2565
|
+
paymentMethodFor(
|
|
2566
|
+
moved.subscriberId,
|
|
2567
|
+
"pm_new_account",
|
|
2568
|
+
"2026-09-02T10:00:00.000Z",
|
|
2569
|
+
"stripe-main"
|
|
2570
|
+
)
|
|
2571
|
+
);
|
|
2572
|
+
await methods.recordConfirmed(
|
|
2573
|
+
paymentMethodFor(
|
|
2574
|
+
stayed.subscriberId,
|
|
2575
|
+
"pm_main",
|
|
2576
|
+
"2026-09-02T10:00:00.000Z",
|
|
2577
|
+
"stripe-main"
|
|
2578
|
+
)
|
|
2579
|
+
);
|
|
2580
|
+
import_strict.default.deepEqual(await methods.accountsInUse(), ["stripe-main"]);
|
|
2581
|
+
});
|
|
2582
|
+
(0, import_node_test.test)("a setup is completed once, and only by the account, session and subscriber it was started with", async (t) => {
|
|
2583
|
+
const methods = harness.adapter.subscriberPaymentMethodRepository;
|
|
2584
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2585
|
+
if (!methods || !createSubscriber) {
|
|
2586
|
+
missing(t, "subscriberPaymentMethods");
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
const { subscriberId } = await createSubscriber({ legalName: "Setup GmbH" });
|
|
2590
|
+
const other = await createSubscriber({ legalName: "Other Tenant GmbH" });
|
|
2591
|
+
await methods.recordSetup({
|
|
2592
|
+
subscriberId,
|
|
2593
|
+
gatewayAccount: "stripe-main",
|
|
2594
|
+
sessionRef: "cs_setup",
|
|
2595
|
+
customerRef: "cus_setup",
|
|
2596
|
+
startedAt: /* @__PURE__ */ new Date("2026-09-15T10:00:00.000Z")
|
|
2597
|
+
});
|
|
2598
|
+
const at = /* @__PURE__ */ new Date("2026-09-15T10:05:00.000Z");
|
|
2599
|
+
const match = { gatewayAccount: "stripe-main", sessionRef: "cs_setup", subscriberId };
|
|
2600
|
+
import_strict.default.equal(
|
|
2601
|
+
await methods.completeSetup({ ...match, subscriberId: other.subscriberId }, at),
|
|
2602
|
+
false
|
|
2603
|
+
);
|
|
2604
|
+
import_strict.default.equal(
|
|
2605
|
+
await methods.completeSetup({ ...match, gatewayAccount: "stripe-old" }, at),
|
|
2606
|
+
false
|
|
2607
|
+
);
|
|
2608
|
+
import_strict.default.equal(
|
|
2609
|
+
await methods.completeSetup({ ...match, sessionRef: "cs_nobody_opened" }, at),
|
|
2610
|
+
false
|
|
2611
|
+
);
|
|
2612
|
+
import_strict.default.equal(await methods.completeSetup(match, at), true);
|
|
2613
|
+
import_strict.default.equal(
|
|
2614
|
+
await methods.completeSetup(match, at),
|
|
2615
|
+
false,
|
|
2616
|
+
"a setup was completed twice"
|
|
2617
|
+
);
|
|
2618
|
+
});
|
|
2619
|
+
(0, import_node_test.test)("a setup completed on a transaction that rolls back is open again", async (t) => {
|
|
2620
|
+
const { adapter } = harness;
|
|
2621
|
+
const methods = adapter.subscriberPaymentMethodRepository;
|
|
2622
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2623
|
+
if (!methods || !createSubscriber) {
|
|
2624
|
+
missing(t, "subscriberPaymentMethods");
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
const { subscriberId } = await createSubscriber({ legalName: "Retry GmbH" });
|
|
2628
|
+
const match = { gatewayAccount: "stripe-main", sessionRef: "cs_retry", subscriberId };
|
|
2629
|
+
await methods.recordSetup({
|
|
2630
|
+
...match,
|
|
2631
|
+
customerRef: "cus_retry",
|
|
2632
|
+
startedAt: /* @__PURE__ */ new Date("2026-09-15T10:00:00.000Z")
|
|
2633
|
+
});
|
|
2634
|
+
const at = /* @__PURE__ */ new Date("2026-09-15T10:05:00.000Z");
|
|
2635
|
+
await import_strict.default.rejects(
|
|
2636
|
+
adapter.transactionRunner.run(async (tx) => {
|
|
2637
|
+
import_strict.default.equal(await methods.completeSetup(match, at, tx), true);
|
|
2638
|
+
throw new Error("recording the payment method failed");
|
|
2639
|
+
}),
|
|
2640
|
+
/recording the payment method failed/
|
|
2641
|
+
);
|
|
2642
|
+
import_strict.default.equal(
|
|
2643
|
+
await methods.completeSetup(match, at),
|
|
2644
|
+
true,
|
|
2645
|
+
"the rollback kept the completion"
|
|
2646
|
+
);
|
|
2647
|
+
});
|
|
2648
|
+
(0, import_node_test.test)("one session is one setup, however often it is recorded", async (t) => {
|
|
2649
|
+
const methods = harness.adapter.subscriberPaymentMethodRepository;
|
|
2650
|
+
const createSubscriber = harness.seed.createSubscriber;
|
|
2651
|
+
if (!methods || !createSubscriber) {
|
|
2652
|
+
missing(t, "subscriberPaymentMethods");
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
const { subscriberId } = await createSubscriber({ legalName: "Once GmbH" });
|
|
2656
|
+
const setup = {
|
|
2657
|
+
subscriberId,
|
|
2658
|
+
gatewayAccount: "stripe-main",
|
|
2659
|
+
sessionRef: "cs_once",
|
|
2660
|
+
customerRef: "cus_once",
|
|
2661
|
+
startedAt: /* @__PURE__ */ new Date("2026-09-15T10:00:00.000Z")
|
|
2662
|
+
};
|
|
2663
|
+
await methods.recordSetup(setup);
|
|
2664
|
+
await import_strict.default.rejects(methods.recordSetup(setup));
|
|
2665
|
+
});
|
|
1754
2666
|
(0, import_node_test.test)("an offer is consumed once, whoever asks first", async (t) => {
|
|
1755
2667
|
const offers = harness.adapter.checkoutOfferRepository;
|
|
1756
2668
|
if (!offers) {
|