@birtalanrobert/commerce 1.0.0

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +601 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +45 -0
  4. package/README.md +79 -0
  5. package/dist/deposits.d.ts +52 -0
  6. package/dist/deposits.d.ts.map +1 -0
  7. package/dist/deposits.js +71 -0
  8. package/dist/deposits.js.map +1 -0
  9. package/dist/index.d.ts +23 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +30 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/migrations/1789800000000-CreateCommerce.d.ts +38 -0
  14. package/dist/migrations/1789800000000-CreateCommerce.d.ts.map +1 -0
  15. package/dist/migrations/1789800000000-CreateCommerce.js +152 -0
  16. package/dist/migrations/1789800000000-CreateCommerce.js.map +1 -0
  17. package/dist/nestjs/commerce.service.d.ts +122 -0
  18. package/dist/nestjs/commerce.service.d.ts.map +1 -0
  19. package/dist/nestjs/commerce.service.js +337 -0
  20. package/dist/nestjs/commerce.service.js.map +1 -0
  21. package/dist/nestjs/index.d.ts +18 -0
  22. package/dist/nestjs/index.d.ts.map +1 -0
  23. package/dist/nestjs/index.js +27 -0
  24. package/dist/nestjs/index.js.map +1 -0
  25. package/dist/nestjs/payment.entity.d.ts +94 -0
  26. package/dist/nestjs/payment.entity.d.ts.map +1 -0
  27. package/dist/nestjs/payment.entity.js +181 -0
  28. package/dist/nestjs/payment.entity.js.map +1 -0
  29. package/dist/nestjs/payout-account.entity.d.ts +41 -0
  30. package/dist/nestjs/payout-account.entity.d.ts.map +1 -0
  31. package/dist/nestjs/payout-account.entity.js +83 -0
  32. package/dist/nestjs/payout-account.entity.js.map +1 -0
  33. package/dist/providers/port.d.ts +97 -0
  34. package/dist/providers/port.d.ts.map +1 -0
  35. package/dist/providers/port.js +16 -0
  36. package/dist/providers/port.js.map +1 -0
  37. package/dist/providers/stripe.d.ts +39 -0
  38. package/dist/providers/stripe.d.ts.map +1 -0
  39. package/dist/providers/stripe.js +221 -0
  40. package/dist/providers/stripe.js.map +1 -0
  41. package/nestjs/package.json +5 -0
  42. package/package.json +49 -0
  43. package/src/deposits.ts +96 -0
  44. package/src/index.ts +40 -0
  45. package/src/migrations/1789800000000-CreateCommerce.ts +156 -0
  46. package/src/nestjs/commerce.service.ts +476 -0
  47. package/src/nestjs/index.ts +24 -0
  48. package/src/nestjs/payment.entity.ts +150 -0
  49. package/src/nestjs/payout-account.entity.ts +56 -0
  50. package/src/providers/port.ts +108 -0
  51. package/src/providers/stripe.ts +274 -0
package/NOTICE ADDED
@@ -0,0 +1,45 @@
1
+ mortar
2
+ Copyright (C) 2026 Robert Birtalan
3
+
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License as published by the Free
6
+ Software Foundation, either version 3 of the License, or (at your option) any
7
+ later version.
8
+
9
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY
10
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11
+ PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
12
+
13
+ You should have received a copy of the GNU Affero General Public License along
14
+ with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ --------------------------------------------------------------------------------
17
+
18
+ RIGHTS RESERVED BY THE COPYRIGHT HOLDER
19
+
20
+ The AGPL is a grant from the copyright holder to everyone else. The copyright
21
+ holder is not a licensee of their own work, and is therefore not bound by these
22
+ terms.
23
+
24
+ Robert Birtalan, as sole copyright holder, retains the right to use, modify and
25
+ distribute this software under any other terms, including within closed-source
26
+ and commercial products, without triggering any obligation under the AGPL.
27
+
28
+ Commercial licences for third parties who cannot or do not wish to comply with
29
+ the AGPL are available on request.
30
+
31
+ --------------------------------------------------------------------------------
32
+
33
+ CONTRIBUTIONS
34
+
35
+ Contributions are not currently accepted.
36
+
37
+ This is deliberate and load-bearing. A contributor retains copyright in their
38
+ contribution unless they assign it. Accepting outside contributions without a
39
+ copyright assignment or a contributor licence agreement would mean the project
40
+ was no longer wholly owned by one copyright holder — and the reserved rights
41
+ above would no longer apply to those parts, because one cannot relicense code
42
+ one does not own.
43
+
44
+ Should contributions be accepted in future, a CLA assigning copyright to the
45
+ maintainer is a prerequisite, not a formality.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @birtalanrobert/commerce
2
+
3
+ Taking money **on a business's behalf**: payout onboarding, card payments,
4
+ manually recorded takings, refunds, and the record of all of it.
5
+
6
+ **We never hold anybody's funds.** The customer pays the business directly and
7
+ our cut is taken on top as an application fee. This is an architectural rule
8
+ rather than a preference — holding third-party money turns a software company
9
+ into a regulated payments business — and everything here follows from it.
10
+
11
+ Not to be confused with `@birtalanrobert/billing`, which is the other direction:
12
+ the business paying _us_ for a subscription. They differ in who pays whom, in
13
+ which provider account, and in what happens when one fails.
14
+
15
+ ## What is decided without a provider
16
+
17
+ The root entry point is pure, because a console shows these numbers while
18
+ somebody drags a slider:
19
+
20
+ ```ts
21
+ import { depositFor, canTakeMoney } from '@birtalanrobert/commerce';
22
+
23
+ depositFor(15_000, { kind: 'percentage', value: 30 }); // 4_500
24
+ canTakeMoney('pending'); // false — the provider has not verified them yet
25
+ ```
26
+
27
+ ## Using it in a NestJS application
28
+
29
+ ```ts
30
+ import { StripeConnect } from '@birtalanrobert/commerce';
31
+ import {
32
+ COMMERCE_PROVIDER,
33
+ CommerceService,
34
+ commerceEntities,
35
+ commerceMigrations,
36
+ } from '@birtalanrobert/commerce/nestjs';
37
+
38
+ @Module({
39
+ providers: [
40
+ {
41
+ provide: COMMERCE_PROVIDER,
42
+ useFactory: (config: AppConfig) =>
43
+ new StripeConnect({
44
+ secretKey: config.STRIPE_SECRET_KEY,
45
+ webhookSecret: config.STRIPE_WEBHOOK_SECRET,
46
+ }),
47
+ inject: [ConfigModule.token()],
48
+ },
49
+ CommerceService,
50
+ ],
51
+ })
52
+ export class PaymentsModule {}
53
+ ```
54
+
55
+ Register `commerceEntities` and `commerceMigrations` with the data source, as
56
+ with every other package here.
57
+
58
+ ## The parts worth knowing before using it
59
+
60
+ - **A business cannot be charged for until the provider says it may be paid
61
+ out.** `take` refuses before the provider is called. A charge that succeeded
62
+ into an account with no destination would leave the customer debited and the
63
+ money nowhere anybody can see it.
64
+ - **`authorized` is not `captured`.** A card held against a no-show fee is
65
+ authorised and charged only if the fee is actually applied — and _that
66
+ decision is a human one_. This package offers the mechanism and never the
67
+ trigger, because charging a customer automatically is how a business loses
68
+ them permanently.
69
+ - **Cash, terminal, voucher and transfer payments are recorded, not
70
+ processed** — and recording them is not a lesser feature. A salon is mostly
71
+ cash, a restaurant takes meal vouchers, a box office takes notes. A report
72
+ counting only what a provider processed tells a business a fraction of its own
73
+ takings while looking complete.
74
+ - **The record outlives the provider.** Amounts, dates, what it was for and who
75
+ decided are stored in full rather than as identifiers to fetch, because a
76
+ business must be able to produce its own takings after the account is closed.
77
+ - **There is no foreign key to whatever was paid for.** One product takes a
78
+ deposit against an appointment, another against a seat, a third against a
79
+ table's tab; a key to any one of them is what would stop this being shared.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What a customer has to pay up front, worked out the way a person would check.
3
+ *
4
+ * **Pure, and at the root entry point on purpose.** A console shows "30% of
5
+ * 150,00 lei is 45,00 lei" while somebody drags a slider, and a booking page
6
+ * shows the same number before anybody types a card into it. Neither may reach
7
+ * for a database, and both must arrive at exactly the number the charge will be.
8
+ */
9
+ /** How a business asks for money before the work. */
10
+ export type DepositKind = 'none' | 'percentage' | 'fixed' | 'full';
11
+ export interface DepositPolicy {
12
+ readonly kind: DepositKind;
13
+ /**
14
+ * Whole percent for `percentage`, minor units for `fixed`, ignored otherwise.
15
+ *
16
+ * Whole percent rather than a fraction because it is typed into a box by a
17
+ * person: "30" is what a tattoo studio says, and `0.3` is what turns into
18
+ * `0.30000000000000004` two operations later.
19
+ */
20
+ readonly value: number;
21
+ }
22
+ /**
23
+ * The deposit for a given total.
24
+ *
25
+ * **Rounded to the nearest minor unit, and never past the total.** A percentage
26
+ * of an odd amount is a fraction of a ban, and a deposit larger than the price
27
+ * is what a misconfigured 150% produces — both are refused here rather than at
28
+ * the provider, where the message is in English and mentions an integer.
29
+ */
30
+ export declare function depositFor(total: number, policy: DepositPolicy): number;
31
+ /** What is still owed after a deposit. Never negative, whatever was paid. */
32
+ export declare const balanceAfter: (total: number, paid: number) => number;
33
+ /**
34
+ * Whether a business may take money at all.
35
+ *
36
+ * The provider's onboarding is the gate, and it is a hard one: funds go
37
+ * directly to the business and our fee is taken on top, so until the provider
38
+ * has verified who they are there is nowhere for the money to go. Every
39
+ * consumer of this package has to answer the same question in front of the
40
+ * same button, which is why it is here rather than written three times.
41
+ */
42
+ export type PayoutStatus = 'none' | 'pending' | 'restricted' | 'ready';
43
+ export declare const canTakeMoney: (status: PayoutStatus) => boolean;
44
+ /**
45
+ * What to tell a business that cannot yet, in the order it becomes true.
46
+ *
47
+ * Returned as a key rather than a sentence: the words belong to whichever
48
+ * product is showing them, and a package that shipped English into a Romanian
49
+ * console would be worse than one that shipped nothing.
50
+ */
51
+ export declare const payoutBlockReason: (status: PayoutStatus) => "not-started" | "in-progress" | "needs-attention" | null;
52
+ //# sourceMappingURL=deposits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deposits.d.ts","sourceRoot":"","sources":["../src/deposits.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,qDAAqD;AACrD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,CAAC;AAEnE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,MAAM,CAuBvE;AAED,6EAA6E;AAC7E,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,MAAmC,CAAC;AAI/F;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,OAAO,CAAC;AAEvE,eAAO,MAAM,YAAY,GAAI,QAAQ,YAAY,KAAG,OAA6B,CAAC;AAElF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,YAAY,KACnB,aAAa,GAAG,aAAa,GAAG,iBAAiB,GAAG,IAWtD,CAAC"}
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ /**
3
+ * What a customer has to pay up front, worked out the way a person would check.
4
+ *
5
+ * **Pure, and at the root entry point on purpose.** A console shows "30% of
6
+ * 150,00 lei is 45,00 lei" while somebody drags a slider, and a booking page
7
+ * shows the same number before anybody types a card into it. Neither may reach
8
+ * for a database, and both must arrive at exactly the number the charge will be.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.payoutBlockReason = exports.canTakeMoney = exports.balanceAfter = void 0;
12
+ exports.depositFor = depositFor;
13
+ /**
14
+ * The deposit for a given total.
15
+ *
16
+ * **Rounded to the nearest minor unit, and never past the total.** A percentage
17
+ * of an odd amount is a fraction of a ban, and a deposit larger than the price
18
+ * is what a misconfigured 150% produces — both are refused here rather than at
19
+ * the provider, where the message is in English and mentions an integer.
20
+ */
21
+ function depositFor(total, policy) {
22
+ if (total <= 0)
23
+ return 0;
24
+ switch (policy.kind) {
25
+ case 'none':
26
+ return 0;
27
+ case 'full':
28
+ return total;
29
+ case 'fixed':
30
+ // A fixed deposit above the price is a configuration mistake, and taking
31
+ // more than the thing costs is the worst possible way to surface it.
32
+ return clamp(Math.round(policy.value), total);
33
+ case 'percentage':
34
+ /*
35
+ * Rounded half away from zero, which is what a person doing it by hand
36
+ * produces. `Math.round` rounds half *up*, which differs on negatives —
37
+ * irrelevant here because a total is never negative, and stated so that
38
+ * the day it is, this is the line to look at.
39
+ */
40
+ return clamp(Math.round((total * clamp(policy.value, 100)) / 100), total);
41
+ default:
42
+ return 0;
43
+ }
44
+ }
45
+ /** What is still owed after a deposit. Never negative, whatever was paid. */
46
+ const balanceAfter = (total, paid) => Math.max(0, total - paid);
47
+ exports.balanceAfter = balanceAfter;
48
+ const clamp = (value, max) => Math.min(Math.max(0, value), max);
49
+ const canTakeMoney = (status) => status === 'ready';
50
+ exports.canTakeMoney = canTakeMoney;
51
+ /**
52
+ * What to tell a business that cannot yet, in the order it becomes true.
53
+ *
54
+ * Returned as a key rather than a sentence: the words belong to whichever
55
+ * product is showing them, and a package that shipped English into a Romanian
56
+ * console would be worse than one that shipped nothing.
57
+ */
58
+ const payoutBlockReason = (status) => {
59
+ switch (status) {
60
+ case 'none':
61
+ return 'not-started';
62
+ case 'pending':
63
+ return 'in-progress';
64
+ case 'restricted':
65
+ return 'needs-attention';
66
+ case 'ready':
67
+ return null;
68
+ }
69
+ };
70
+ exports.payoutBlockReason = payoutBlockReason;
71
+ //# sourceMappingURL=deposits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deposits.js","sourceRoot":"","sources":["../src/deposits.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAyBH,gCAuBC;AA/BD;;;;;;;GAOG;AACH,SAAgB,UAAU,CAAC,KAAa,EAAE,MAAqB;IAC7D,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAEzB,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,MAAM;YACT,OAAO,CAAC,CAAC;QACX,KAAK,MAAM;YACT,OAAO,KAAK,CAAC;QACf,KAAK,OAAO;YACV,yEAAyE;YACzE,qEAAqE;YACrE,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;QAChD,KAAK,YAAY;YACf;;;;;eAKG;YACH,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;QAC5E;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED,6EAA6E;AACtE,MAAM,YAAY,GAAG,CAAC,KAAa,EAAE,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,CAAC;AAAlF,QAAA,YAAY,gBAAsE;AAE/F,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,GAAW,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC;AAajF,MAAM,YAAY,GAAG,CAAC,MAAoB,EAAW,EAAE,CAAC,MAAM,KAAK,OAAO,CAAC;AAArE,QAAA,YAAY,gBAAyD;AAElF;;;;;;GAMG;AACI,MAAM,iBAAiB,GAAG,CAC/B,MAAoB,EACsC,EAAE;IAC5D,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,MAAM;YACT,OAAO,aAAa,CAAC;QACvB,KAAK,SAAS;YACZ,OAAO,aAAa,CAAC;QACvB,KAAK,YAAY;YACf,OAAO,iBAAiB,CAAC;QAC3B,KAAK,OAAO;YACV,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC,CAAC;AAbW,QAAA,iBAAiB,qBAa5B"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Taking money on a business's behalf.
3
+ *
4
+ * **We never hold anybody's funds.** The customer pays the business directly
5
+ * and our fee is taken on top as an application fee — a hard architectural rule
6
+ * rather than a preference, because holding third-party money turns a software
7
+ * company into a regulated payments business. Every design decision here
8
+ * follows from it.
9
+ *
10
+ * Not to be confused with `@birtalanrobert/billing`, which is the other
11
+ * direction: the business paying *us* for a subscription. Conflating the two is
12
+ * the mistake that makes both hard to reason about — they differ in who pays
13
+ * whom, in which Stripe account, and in what happens when one fails.
14
+ *
15
+ * **This entry point is pure.** What a deposit comes to and whether a business
16
+ * may sell yet are decided without a database or a provider, because a console
17
+ * shows both while somebody drags a slider. Everything needing storage or
18
+ * somebody else's money-moving licence is behind `/nestjs`.
19
+ */
20
+ export { balanceAfter, canTakeMoney, depositFor, payoutBlockReason, type DepositKind, type DepositPolicy, type PayoutStatus, } from './deposits';
21
+ export type { ChargeRequest, ChargeResult, OnboardingLink, PaymentProvider, ProviderAccount, ProviderEvent, RefundRequest, } from './providers/port';
22
+ export { StripeConnect, type StripeConnectOptions } from './providers/stripe';
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,aAAa,EACb,YAAY,EACZ,cAAc,EACd,eAAe,EACf,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StripeConnect = exports.payoutBlockReason = exports.depositFor = exports.canTakeMoney = exports.balanceAfter = void 0;
4
+ /**
5
+ * Taking money on a business's behalf.
6
+ *
7
+ * **We never hold anybody's funds.** The customer pays the business directly
8
+ * and our fee is taken on top as an application fee — a hard architectural rule
9
+ * rather than a preference, because holding third-party money turns a software
10
+ * company into a regulated payments business. Every design decision here
11
+ * follows from it.
12
+ *
13
+ * Not to be confused with `@birtalanrobert/billing`, which is the other
14
+ * direction: the business paying *us* for a subscription. Conflating the two is
15
+ * the mistake that makes both hard to reason about — they differ in who pays
16
+ * whom, in which Stripe account, and in what happens when one fails.
17
+ *
18
+ * **This entry point is pure.** What a deposit comes to and whether a business
19
+ * may sell yet are decided without a database or a provider, because a console
20
+ * shows both while somebody drags a slider. Everything needing storage or
21
+ * somebody else's money-moving licence is behind `/nestjs`.
22
+ */
23
+ var deposits_1 = require("./deposits");
24
+ Object.defineProperty(exports, "balanceAfter", { enumerable: true, get: function () { return deposits_1.balanceAfter; } });
25
+ Object.defineProperty(exports, "canTakeMoney", { enumerable: true, get: function () { return deposits_1.canTakeMoney; } });
26
+ Object.defineProperty(exports, "depositFor", { enumerable: true, get: function () { return deposits_1.depositFor; } });
27
+ Object.defineProperty(exports, "payoutBlockReason", { enumerable: true, get: function () { return deposits_1.payoutBlockReason; } });
28
+ var stripe_1 = require("./providers/stripe");
29
+ Object.defineProperty(exports, "StripeConnect", { enumerable: true, get: function () { return stripe_1.StripeConnect; } });
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,uCAQoB;AAPlB,wGAAA,YAAY,OAAA;AACZ,wGAAA,YAAY,OAAA;AACZ,sGAAA,UAAU,OAAA;AACV,6GAAA,iBAAiB,OAAA;AAgBnB,6CAA8E;AAArE,uGAAA,aAAa,OAAA"}
@@ -0,0 +1,38 @@
1
+ import type { MigrationInterface, QueryRunner } from 'typeorm';
2
+ /**
3
+ * Taking money on a business's behalf.
4
+ *
5
+ * **We never hold anybody's funds.** The customer pays the business directly
6
+ * and our fee is taken on top — a hard architectural rule rather than a
7
+ * preference, because holding third-party money turns a software company into a
8
+ * regulated payments business. Everything here follows from that.
9
+ *
10
+ * ## Three tables, and why each is separate
11
+ *
12
+ * `mortar_payout_accounts` is the gate. Until the provider has verified who a
13
+ * business is, there is nowhere for a payment to land, and every product that
14
+ * sells on somebody's behalf has to answer that same question in front of the
15
+ * same button.
16
+ *
17
+ * `mortar_payments` is the record, and it **outlives the provider**: amounts,
18
+ * dates, what it was for and who decided are stored in full rather than as
19
+ * identifiers to fetch later. A business must be able to produce its own
20
+ * takings years after the provider account is closed or the vendor replaced.
21
+ *
22
+ * `mortar_payment_refunds` is one row per act of giving money back. Several
23
+ * partial refunds against one payment is ordinary, and a single column cannot
24
+ * say when each happened or why.
25
+ *
26
+ * ## No foreign key to whatever was paid for
27
+ *
28
+ * One product takes a deposit against an appointment, another against a seat, a
29
+ * third against a table's tab. A key to any one of them is exactly what would
30
+ * stop this table being shared, so the subject is a string the owning product
31
+ * writes and reads.
32
+ */
33
+ export declare class CreateCommerce1789800000000 implements MigrationInterface {
34
+ name: string;
35
+ up(queryRunner: QueryRunner): Promise<void>;
36
+ down(queryRunner: QueryRunner): Promise<void>;
37
+ }
38
+ //# sourceMappingURL=1789800000000-CreateCommerce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"1789800000000-CreateCommerce.d.ts","sourceRoot":"","sources":["../../src/migrations/1789800000000-CreateCommerce.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAG/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,2BAA4B,YAAW,kBAAkB;IACpE,IAAI,SAAiC;IAExB,EAAE,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAiH3C,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAK3D"}
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreateCommerce1789800000000 = void 0;
4
+ const tenancy_1 = require("@birtalanrobert/tenancy");
5
+ /**
6
+ * Taking money on a business's behalf.
7
+ *
8
+ * **We never hold anybody's funds.** The customer pays the business directly
9
+ * and our fee is taken on top — a hard architectural rule rather than a
10
+ * preference, because holding third-party money turns a software company into a
11
+ * regulated payments business. Everything here follows from that.
12
+ *
13
+ * ## Three tables, and why each is separate
14
+ *
15
+ * `mortar_payout_accounts` is the gate. Until the provider has verified who a
16
+ * business is, there is nowhere for a payment to land, and every product that
17
+ * sells on somebody's behalf has to answer that same question in front of the
18
+ * same button.
19
+ *
20
+ * `mortar_payments` is the record, and it **outlives the provider**: amounts,
21
+ * dates, what it was for and who decided are stored in full rather than as
22
+ * identifiers to fetch later. A business must be able to produce its own
23
+ * takings years after the provider account is closed or the vendor replaced.
24
+ *
25
+ * `mortar_payment_refunds` is one row per act of giving money back. Several
26
+ * partial refunds against one payment is ordinary, and a single column cannot
27
+ * say when each happened or why.
28
+ *
29
+ * ## No foreign key to whatever was paid for
30
+ *
31
+ * One product takes a deposit against an appointment, another against a seat, a
32
+ * third against a table's tab. A key to any one of them is exactly what would
33
+ * stop this table being shared, so the subject is a string the owning product
34
+ * writes and reads.
35
+ */
36
+ class CreateCommerce1789800000000 {
37
+ name = 'CreateCommerce1789800000000';
38
+ async up(queryRunner) {
39
+ await queryRunner.query(`
40
+ CREATE TABLE "mortar_payout_accounts" (
41
+ "id" uuid NOT NULL DEFAULT gen_random_uuid(),
42
+ "created_at" timestamptz NOT NULL DEFAULT now(),
43
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
44
+ "tenant_id" uuid NOT NULL,
45
+ "provider" varchar(32) NOT NULL DEFAULT 'stripe',
46
+ "external_id" varchar(128) NOT NULL,
47
+ "status" varchar(16) NOT NULL DEFAULT 'pending',
48
+ -- What the provider still wants, in its own words. "A photograph of the
49
+ -- director's identity document" is actionable; "restricted" is a
50
+ -- support conversation.
51
+ "requirements" jsonb NOT NULL DEFAULT '[]'::jsonb,
52
+ "ready_at" timestamptz,
53
+ CONSTRAINT "pk_payout_accounts" PRIMARY KEY ("id"),
54
+ CONSTRAINT "uq_payout_accounts_tenant" UNIQUE ("tenant_id", "provider"),
55
+ CONSTRAINT "ck_payout_accounts_status"
56
+ CHECK ("status" IN ('none', 'pending', 'restricted', 'ready'))
57
+ )
58
+ `);
59
+ /* A webhook arrives naming the provider's account, not ours. */
60
+ await queryRunner.query(`
61
+ CREATE INDEX "ix_payout_accounts_external"
62
+ ON "mortar_payout_accounts" ("provider", "external_id")
63
+ `);
64
+ await queryRunner.query(`
65
+ CREATE TABLE "mortar_payments" (
66
+ "id" uuid NOT NULL DEFAULT gen_random_uuid(),
67
+ "created_at" timestamptz NOT NULL DEFAULT now(),
68
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
69
+ "tenant_id" uuid NOT NULL,
70
+ -- What it was for, as the owning product names it. No foreign key: see
71
+ -- the note above.
72
+ "subject" varchar(160) NOT NULL,
73
+ -- 'card' is the only one this package processes. The rest are recorded
74
+ -- rather than taken, and recording them is not a lesser feature: a
75
+ -- report that counts only what a provider processed tells a salon a
76
+ -- fraction of its own takings and looks complete.
77
+ "method" varchar(16) NOT NULL,
78
+ "state" varchar(24) NOT NULL DEFAULT 'pending',
79
+ "amount" bigint NOT NULL,
80
+ "currency" varchar(3) NOT NULL,
81
+ -- Recorded even when zero: "charged nothing" and "nobody wrote down
82
+ -- what was charged" are different facts.
83
+ "application_fee" bigint NOT NULL DEFAULT 0,
84
+ "refunded" bigint NOT NULL DEFAULT 0,
85
+ "provider" varchar(32),
86
+ "external_id" varchar(128),
87
+ -- "Visa ending 4242". Never the number, never anything chargeable from
88
+ -- a database dump.
89
+ "instrument" varchar(40),
90
+ "taken_at" timestamptz,
91
+ "detail" varchar(400),
92
+ "recorded_by" uuid,
93
+ CONSTRAINT "pk_payments" PRIMARY KEY ("id"),
94
+ CONSTRAINT "uq_payments_tenant_id" UNIQUE ("tenant_id", "id"),
95
+ CONSTRAINT "ck_payments_method"
96
+ CHECK ("method" IN ('card', 'cash', 'terminal', 'voucher', 'transfer')),
97
+ CONSTRAINT "ck_payments_state"
98
+ CHECK ("state" IN ('pending', 'authorized', 'captured', 'failed',
99
+ 'refunded', 'partially_refunded', 'cancelled')),
100
+ -- Money is never negative here, and nothing may be given back twice.
101
+ CONSTRAINT "ck_payments_amount" CHECK ("amount" >= 0),
102
+ CONSTRAINT "ck_payments_refunded"
103
+ CHECK ("refunded" >= 0 AND "refunded" <= "amount")
104
+ )
105
+ `);
106
+ await queryRunner.query(`
107
+ CREATE INDEX "ix_payments_subject" ON "mortar_payments" ("tenant_id", "subject")
108
+ `);
109
+ /* Every revenue report reads this: a tenant's takings over a period. */
110
+ await queryRunner.query(`
111
+ CREATE INDEX "ix_payments_taken" ON "mortar_payments" ("tenant_id", "taken_at")
112
+ `);
113
+ /* And a webhook arrives naming the provider's payment. */
114
+ await queryRunner.query(`
115
+ CREATE INDEX "ix_payments_external" ON "mortar_payments" ("provider", "external_id")
116
+ `);
117
+ await queryRunner.query(`
118
+ CREATE TABLE "mortar_payment_refunds" (
119
+ "id" uuid NOT NULL DEFAULT gen_random_uuid(),
120
+ "created_at" timestamptz NOT NULL DEFAULT now(),
121
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
122
+ "tenant_id" uuid NOT NULL,
123
+ "payment_id" uuid NOT NULL,
124
+ "amount" bigint NOT NULL,
125
+ -- Required. "We refunded her ninety lei in March" is a question
126
+ -- somebody asks a year later, and a blank reason cannot be defended.
127
+ "reason" varchar(400) NOT NULL,
128
+ "external_id" varchar(128),
129
+ "refunded_by" uuid,
130
+ CONSTRAINT "pk_payment_refunds" PRIMARY KEY ("id"),
131
+ CONSTRAINT "fk_payment_refunds_payment" FOREIGN KEY ("tenant_id", "payment_id")
132
+ REFERENCES "mortar_payments" ("tenant_id", "id") ON DELETE RESTRICT,
133
+ CONSTRAINT "ck_payment_refunds_amount" CHECK ("amount" > 0)
134
+ )
135
+ `);
136
+ await queryRunner.query(`
137
+ CREATE INDEX "ix_payment_refunds_payment"
138
+ ON "mortar_payment_refunds" ("tenant_id", "payment_id")
139
+ `);
140
+ for (const table of ['mortar_payout_accounts', 'mortar_payments', 'mortar_payment_refunds']) {
141
+ for (const statement of (0, tenancy_1.enableRlsSql)(table))
142
+ await queryRunner.query(statement);
143
+ }
144
+ }
145
+ async down(queryRunner) {
146
+ await queryRunner.query(`DROP TABLE IF EXISTS "mortar_payment_refunds" CASCADE`);
147
+ await queryRunner.query(`DROP TABLE IF EXISTS "mortar_payments" CASCADE`);
148
+ await queryRunner.query(`DROP TABLE IF EXISTS "mortar_payout_accounts" CASCADE`);
149
+ }
150
+ }
151
+ exports.CreateCommerce1789800000000 = CreateCommerce1789800000000;
152
+ //# sourceMappingURL=1789800000000-CreateCommerce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"1789800000000-CreateCommerce.js","sourceRoot":"","sources":["../../src/migrations/1789800000000-CreateCommerce.ts"],"names":[],"mappings":";;;AACA,qDAAuD;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAa,2BAA2B;IACtC,IAAI,GAAG,6BAA6B,CAAC;IAE9B,KAAK,CAAC,EAAE,CAAC,WAAwB;QACtC,MAAM,WAAW,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;KAmBvB,CAAC,CAAC;QAEH,gEAAgE;QAChE,MAAM,WAAW,CAAC,KAAK,CAAC;;;KAGvB,CAAC,CAAC;QAEH,MAAM,WAAW,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyCvB,CAAC,CAAC;QAEH,MAAM,WAAW,CAAC,KAAK,CAAC;;KAEvB,CAAC,CAAC;QACH,wEAAwE;QACxE,MAAM,WAAW,CAAC,KAAK,CAAC;;KAEvB,CAAC,CAAC;QACH,0DAA0D;QAC1D,MAAM,WAAW,CAAC,KAAK,CAAC;;KAEvB,CAAC,CAAC;QAEH,MAAM,WAAW,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;KAkBvB,CAAC,CAAC;QAEH,MAAM,WAAW,CAAC,KAAK,CAAC;;;KAGvB,CAAC,CAAC;QAEH,KAAK,MAAM,KAAK,IAAI,CAAC,wBAAwB,EAAE,iBAAiB,EAAE,wBAAwB,CAAC,EAAE,CAAC;YAC5F,KAAK,MAAM,SAAS,IAAI,IAAA,sBAAY,EAAC,KAAK,CAAC;gBAAE,MAAM,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,WAAwB;QACxC,MAAM,WAAW,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAC;QACjF,MAAM,WAAW,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;QAC1E,MAAM,WAAW,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAC;IACnF,CAAC;CACF;AAzHD,kEAyHC"}
@@ -0,0 +1,122 @@
1
+ import type { DataSource } from 'typeorm';
2
+ import { type PayoutStatus } from '../deposits';
3
+ import type { PaymentProvider, ProviderEvent } from '../providers/port';
4
+ import { PayoutAccount } from './payout-account.entity';
5
+ import { Payment, PaymentRefund, type PaymentMethod } from './payment.entity';
6
+ /** The provider this deployment uses, injected so tests can supply their own. */
7
+ export declare const COMMERCE_PROVIDER: unique symbol;
8
+ export interface TakePayment {
9
+ readonly subject: string;
10
+ readonly amount: number;
11
+ readonly currency: string;
12
+ readonly applicationFee?: number;
13
+ readonly description?: string;
14
+ /** False holds the money instead of taking it. See `capture`. */
15
+ readonly capture?: boolean;
16
+ /**
17
+ * Makes the charge idempotent across retries of the same act.
18
+ *
19
+ * A person pressing a button twice on a slow connection is the ordinary case,
20
+ * not an exotic one, and the second press must not produce a second charge.
21
+ */
22
+ readonly reference: string;
23
+ }
24
+ export interface RecordPayment {
25
+ readonly subject: string;
26
+ readonly amount: number;
27
+ readonly currency: string;
28
+ readonly method: Exclude<PaymentMethod, 'card'>;
29
+ readonly detail?: string;
30
+ }
31
+ /**
32
+ * Money between a customer and a business, and the record of it.
33
+ *
34
+ * **We never hold their funds** — the customer pays the business directly and
35
+ * our fee is taken on top — so almost everything here is about the *record*
36
+ * rather than the movement. That record has to be complete enough to produce a
37
+ * business's own takings years later, after the provider account is closed.
38
+ *
39
+ * Every method runs inside the tenant's policy, reads included: these tables are
40
+ * under row-level security, and an unbound read returns *nothing* rather than
41
+ * failing — which here means a revenue report of zero for a business that took
42
+ * money all month.
43
+ */
44
+ export declare class CommerceService {
45
+ private readonly dataSource;
46
+ private readonly provider;
47
+ constructor(dataSource: DataSource, provider: PaymentProvider);
48
+ /** Where a business's money goes, and whether the provider will send it yet. */
49
+ payoutAccount(tenantId: string): Promise<PayoutAccount | null>;
50
+ payoutStatus(tenantId: string): Promise<PayoutStatus>;
51
+ /**
52
+ * Starts or resumes onboarding, creating the provider's account if needed.
53
+ *
54
+ * Called again as often as somebody presses the button: onboarding is a form
55
+ * people abandon and come back to, and the link is short-lived, so "resume"
56
+ * is the common case rather than the exception.
57
+ */
58
+ startOnboarding(tenantId: string, options: {
59
+ country: string;
60
+ email?: string;
61
+ returnUrl: string;
62
+ refreshUrl: string;
63
+ }): Promise<{
64
+ url: string;
65
+ expiresAt: Date;
66
+ }>;
67
+ /** Asks the provider where onboarding stands, and records the answer. */
68
+ refreshPayoutAccount(tenantId: string): Promise<PayoutStatus>;
69
+ /**
70
+ * Charges a card, or holds one.
71
+ *
72
+ * Refused before the provider is called if the business cannot be paid out:
73
+ * a charge that succeeds into an account with no destination leaves the
74
+ * customer debited and the money nowhere anybody can see it, and the first
75
+ * the business hears is asking where it went.
76
+ */
77
+ take(tenantId: string, input: TakePayment): Promise<Payment>;
78
+ /**
79
+ * Writes down money that arrived some other way.
80
+ *
81
+ * Cash at the counter, a card terminal, a meal voucher, a bank transfer.
82
+ * **Recording is not a lesser feature**: a salon is mostly cash, a restaurant
83
+ * takes vouchers, a box office takes notes — and a report that counted only
84
+ * what a provider processed would tell a business a fraction of its own
85
+ * takings while looking complete.
86
+ */
87
+ record(tenantId: string, input: RecordPayment): Promise<Payment>;
88
+ /**
89
+ * Takes money that was only held.
90
+ *
91
+ * **The human decision has been made by the time this is called.** Charging a
92
+ * no-show fee automatically is how a business loses that customer
93
+ * permanently, so this package offers the mechanism and never the trigger.
94
+ */
95
+ capture(tenantId: string, paymentId: string, amount?: number): Promise<Payment>;
96
+ /** Lets a held card go without taking anything. */
97
+ release(tenantId: string, paymentId: string): Promise<Payment>;
98
+ /**
99
+ * Refunds some or all of a payment, with a reason.
100
+ *
101
+ * The reason is required because "we refunded her ninety lei in March" is a
102
+ * question somebody asks a year later, and a blank answer cannot be defended.
103
+ */
104
+ refund(tenantId: string, paymentId: string, amount: number, reason: string): Promise<Payment>;
105
+ /** Everything taken for one thing: a booking, an order, a tab. */
106
+ forSubject(tenantId: string, subject: string): Promise<Payment[]>;
107
+ /** What a payment has had given back, and why. */
108
+ refundsFor(tenantId: string, paymentId: string): Promise<PaymentRefund[]>;
109
+ /**
110
+ * Records what a webhook said, whatever it was about.
111
+ *
112
+ * A payment the deployment has no record of is **ignored rather than
113
+ * inserted**: it belongs to another environment sharing the provider account,
114
+ * and inventing a row for it would put another system's money in this one's
115
+ * books.
116
+ */
117
+ settle(event: ProviderEvent): Promise<Payment | PayoutAccount | undefined>;
118
+ private find;
119
+ private update;
120
+ private saveAccount;
121
+ }
122
+ //# sourceMappingURL=commerce.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commerce.service.d.ts","sourceRoot":"","sources":["../../src/nestjs/commerce.service.ts"],"names":[],"mappings":"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"}