@softure-ai/billing 0.0.0-stage → 0.1.6

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 (295) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +695 -2
  4. package/dist/calendar.d.ts +26 -0
  5. package/dist/calendar.d.ts.map +1 -0
  6. package/dist/calendar.js +81 -0
  7. package/dist/calendar.js.map +1 -0
  8. package/dist/contract.d.ts +187 -0
  9. package/dist/contract.d.ts.map +1 -0
  10. package/dist/contract.js +6 -0
  11. package/dist/contract.js.map +1 -0
  12. package/dist/currency-digits.d.ts +3 -0
  13. package/dist/currency-digits.d.ts.map +1 -0
  14. package/dist/currency-digits.js +32 -0
  15. package/dist/currency-digits.js.map +1 -0
  16. package/dist/entitlement.d.ts +25 -0
  17. package/dist/entitlement.d.ts.map +1 -0
  18. package/dist/entitlement.js +75 -0
  19. package/dist/entitlement.js.map +1 -0
  20. package/dist/fields.d.ts +25 -0
  21. package/dist/fields.d.ts.map +1 -0
  22. package/dist/fields.js +27 -0
  23. package/dist/fields.js.map +1 -0
  24. package/dist/index.d.ts +274 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +73 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/invoice.d.ts +29 -0
  29. package/dist/invoice.d.ts.map +1 -0
  30. package/dist/invoice.js +35 -0
  31. package/dist/invoice.js.map +1 -0
  32. package/dist/mailing/index.d.ts +2 -0
  33. package/dist/mailing/index.d.ts.map +1 -0
  34. package/dist/mailing/index.js +4 -0
  35. package/dist/mailing/index.js.map +1 -0
  36. package/dist/mailing/reminder-mail.d.ts +50 -0
  37. package/dist/mailing/reminder-mail.d.ts.map +1 -0
  38. package/dist/mailing/reminder-mail.js +71 -0
  39. package/dist/mailing/reminder-mail.js.map +1 -0
  40. package/dist/manual.d.ts +12 -0
  41. package/dist/manual.d.ts.map +1 -0
  42. package/dist/manual.js +22 -0
  43. package/dist/manual.js.map +1 -0
  44. package/dist/messages/en.d.ts +176 -0
  45. package/dist/messages/en.d.ts.map +1 -0
  46. package/dist/messages/en.js +151 -0
  47. package/dist/messages/en.js.map +1 -0
  48. package/dist/messages/index.d.ts +359 -0
  49. package/dist/messages/index.d.ts.map +1 -0
  50. package/dist/messages/index.js +14 -0
  51. package/dist/messages/index.js.map +1 -0
  52. package/dist/messages/pl.d.ts +3 -0
  53. package/dist/messages/pl.d.ts.map +1 -0
  54. package/dist/messages/pl.js +151 -0
  55. package/dist/messages/pl.js.map +1 -0
  56. package/dist/next/access.d.ts +16 -0
  57. package/dist/next/access.d.ts.map +1 -0
  58. package/dist/next/access.js +24 -0
  59. package/dist/next/access.js.map +1 -0
  60. package/dist/next/actions.d.ts +24 -0
  61. package/dist/next/actions.d.ts.map +1 -0
  62. package/dist/next/actions.js +158 -0
  63. package/dist/next/actions.js.map +1 -0
  64. package/dist/next/context.d.ts +4 -0
  65. package/dist/next/context.d.ts.map +1 -0
  66. package/dist/next/context.js +17 -0
  67. package/dist/next/context.js.map +1 -0
  68. package/dist/next/current-entitlement.d.ts +18 -0
  69. package/dist/next/current-entitlement.d.ts.map +1 -0
  70. package/dist/next/current-entitlement.js +26 -0
  71. package/dist/next/current-entitlement.js.map +1 -0
  72. package/dist/next/index.d.ts +8 -0
  73. package/dist/next/index.d.ts.map +1 -0
  74. package/dist/next/index.js +12 -0
  75. package/dist/next/index.js.map +1 -0
  76. package/dist/next/pages.d.ts +24 -0
  77. package/dist/next/pages.d.ts.map +1 -0
  78. package/dist/next/pages.js +166 -0
  79. package/dist/next/pages.js.map +1 -0
  80. package/dist/next/pricing.d.ts +12 -0
  81. package/dist/next/pricing.d.ts.map +1 -0
  82. package/dist/next/pricing.js +20 -0
  83. package/dist/next/pricing.js.map +1 -0
  84. package/dist/next/route.d.ts +11 -0
  85. package/dist/next/route.d.ts.map +1 -0
  86. package/dist/next/route.js +56 -0
  87. package/dist/next/route.js.map +1 -0
  88. package/dist/options.d.ts +85 -0
  89. package/dist/options.d.ts.map +1 -0
  90. package/dist/options.js +121 -0
  91. package/dist/options.js.map +1 -0
  92. package/dist/payment.d.ts +61 -0
  93. package/dist/payment.d.ts.map +1 -0
  94. package/dist/payment.js +12 -0
  95. package/dist/payment.js.map +1 -0
  96. package/dist/plans.d.ts +20 -0
  97. package/dist/plans.d.ts.map +1 -0
  98. package/dist/plans.js +53 -0
  99. package/dist/plans.js.map +1 -0
  100. package/dist/price.d.ts +12 -0
  101. package/dist/price.d.ts.map +1 -0
  102. package/dist/price.js +37 -0
  103. package/dist/price.js.map +1 -0
  104. package/dist/refund.d.ts +70 -0
  105. package/dist/refund.d.ts.map +1 -0
  106. package/dist/refund.js +109 -0
  107. package/dist/refund.js.map +1 -0
  108. package/dist/reminder.d.ts +30 -0
  109. package/dist/reminder.d.ts.map +1 -0
  110. package/dist/reminder.js +34 -0
  111. package/dist/reminder.js.map +1 -0
  112. package/dist/schema.d.ts +1023 -0
  113. package/dist/schema.d.ts.map +1 -0
  114. package/dist/schema.js +77 -0
  115. package/dist/schema.js.map +1 -0
  116. package/dist/scripts/entitlement-scripts.d.ts +17 -0
  117. package/dist/scripts/entitlement-scripts.d.ts.map +1 -0
  118. package/dist/scripts/entitlement-scripts.js +167 -0
  119. package/dist/scripts/entitlement-scripts.js.map +1 -0
  120. package/dist/scripts/index.d.ts +3 -0
  121. package/dist/scripts/index.d.ts.map +1 -0
  122. package/dist/scripts/index.js +5 -0
  123. package/dist/scripts/index.js.map +1 -0
  124. package/dist/scripts/plan-scripts.d.ts +19 -0
  125. package/dist/scripts/plan-scripts.d.ts.map +1 -0
  126. package/dist/scripts/plan-scripts.js +106 -0
  127. package/dist/scripts/plan-scripts.js.map +1 -0
  128. package/dist/server/entitlements.d.ts +69 -0
  129. package/dist/server/entitlements.d.ts.map +1 -0
  130. package/dist/server/entitlements.js +202 -0
  131. package/dist/server/entitlements.js.map +1 -0
  132. package/dist/server/grants.d.ts +74 -0
  133. package/dist/server/grants.d.ts.map +1 -0
  134. package/dist/server/grants.js +174 -0
  135. package/dist/server/grants.js.map +1 -0
  136. package/dist/server/health.d.ts +3 -0
  137. package/dist/server/health.d.ts.map +1 -0
  138. package/dist/server/health.js +17 -0
  139. package/dist/server/health.js.map +1 -0
  140. package/dist/server/index.d.ts +12 -0
  141. package/dist/server/index.d.ts.map +1 -0
  142. package/dist/server/index.js +14 -0
  143. package/dist/server/index.js.map +1 -0
  144. package/dist/server/options.d.ts +20 -0
  145. package/dist/server/options.d.ts.map +1 -0
  146. package/dist/server/options.js +38 -0
  147. package/dist/server/options.js.map +1 -0
  148. package/dist/server/payments.d.ts +140 -0
  149. package/dist/server/payments.d.ts.map +1 -0
  150. package/dist/server/payments.js +339 -0
  151. package/dist/server/payments.js.map +1 -0
  152. package/dist/server/plans.d.ts +50 -0
  153. package/dist/server/plans.d.ts.map +1 -0
  154. package/dist/server/plans.js +129 -0
  155. package/dist/server/plans.js.map +1 -0
  156. package/dist/server/privacy.d.ts +77 -0
  157. package/dist/server/privacy.d.ts.map +1 -0
  158. package/dist/server/privacy.js +110 -0
  159. package/dist/server/privacy.js.map +1 -0
  160. package/dist/server/reminders.d.ts +20 -0
  161. package/dist/server/reminders.d.ts.map +1 -0
  162. package/dist/server/reminders.js +85 -0
  163. package/dist/server/reminders.js.map +1 -0
  164. package/dist/server/requests.d.ts +80 -0
  165. package/dist/server/requests.d.ts.map +1 -0
  166. package/dist/server/requests.js +155 -0
  167. package/dist/server/requests.js.map +1 -0
  168. package/dist/server/setup.d.ts +9 -0
  169. package/dist/server/setup.d.ts.map +1 -0
  170. package/dist/server/setup.js +34 -0
  171. package/dist/server/setup.js.map +1 -0
  172. package/dist/server/take-back.d.ts +80 -0
  173. package/dist/server/take-back.d.ts.map +1 -0
  174. package/dist/server/take-back.js +138 -0
  175. package/dist/server/take-back.js.map +1 -0
  176. package/dist/server/user-id.d.ts +4 -0
  177. package/dist/server/user-id.d.ts.map +1 -0
  178. package/dist/server/user-id.js +11 -0
  179. package/dist/server/user-id.js.map +1 -0
  180. package/dist/stripe-currency.d.ts +27 -0
  181. package/dist/stripe-currency.d.ts.map +1 -0
  182. package/dist/stripe-currency.js +57 -0
  183. package/dist/stripe-currency.js.map +1 -0
  184. package/dist/stripe-webhook.d.ts +101 -0
  185. package/dist/stripe-webhook.d.ts.map +1 -0
  186. package/dist/stripe-webhook.js +209 -0
  187. package/dist/stripe-webhook.js.map +1 -0
  188. package/dist/stripe.d.ts +25 -0
  189. package/dist/stripe.d.ts.map +1 -0
  190. package/dist/stripe.js +116 -0
  191. package/dist/stripe.js.map +1 -0
  192. package/dist/ui/access-badge.d.ts +16 -0
  193. package/dist/ui/access-badge.d.ts.map +1 -0
  194. package/dist/ui/access-badge.js +43 -0
  195. package/dist/ui/access-badge.js.map +1 -0
  196. package/dist/ui/access-notice.d.ts +20 -0
  197. package/dist/ui/access-notice.d.ts.map +1 -0
  198. package/dist/ui/access-notice.js +41 -0
  199. package/dist/ui/access-notice.js.map +1 -0
  200. package/dist/ui/format.d.ts +11 -0
  201. package/dist/ui/format.d.ts.map +1 -0
  202. package/dist/ui/format.js +21 -0
  203. package/dist/ui/format.js.map +1 -0
  204. package/dist/ui/grant-form.d.ts +18 -0
  205. package/dist/ui/grant-form.d.ts.map +1 -0
  206. package/dist/ui/grant-form.js +23 -0
  207. package/dist/ui/grant-form.js.map +1 -0
  208. package/dist/ui/grant-history.d.ts +39 -0
  209. package/dist/ui/grant-history.d.ts.map +1 -0
  210. package/dist/ui/grant-history.js +36 -0
  211. package/dist/ui/grant-history.js.map +1 -0
  212. package/dist/ui/index.d.ts +9 -0
  213. package/dist/ui/index.d.ts.map +1 -0
  214. package/dist/ui/index.js +12 -0
  215. package/dist/ui/index.js.map +1 -0
  216. package/dist/ui/payment-form.d.ts +23 -0
  217. package/dist/ui/payment-form.d.ts.map +1 -0
  218. package/dist/ui/payment-form.js +34 -0
  219. package/dist/ui/payment-form.js.map +1 -0
  220. package/dist/ui/payment-requests.d.ts +30 -0
  221. package/dist/ui/payment-requests.d.ts.map +1 -0
  222. package/dist/ui/payment-requests.js +31 -0
  223. package/dist/ui/payment-requests.js.map +1 -0
  224. package/dist/ui/pricing-tiles.d.ts +22 -0
  225. package/dist/ui/pricing-tiles.d.ts.map +1 -0
  226. package/dist/ui/pricing-tiles.js +40 -0
  227. package/dist/ui/pricing-tiles.js.map +1 -0
  228. package/migrations/0001_create_entitlements.sql +16 -0
  229. package/migrations/0002_create_payments.sql +30 -0
  230. package/migrations/0003_record_payment_grants.sql +25 -0
  231. package/migrations/0004_create_requests_and_grants.sql +58 -0
  232. package/migrations/0005_record_refunded_amounts.sql +18 -0
  233. package/migrations/0006_record_request_handover_and_prices.sql +38 -0
  234. package/migrations/0007_record_failed_refunds.sql +27 -0
  235. package/migrations/0008_record_request_handover_claims.sql +9 -0
  236. package/migrations/0009_record_pending_charge_states.sql +16 -0
  237. package/module.json +23 -0
  238. package/package.json +85 -4
  239. package/src/calendar.ts +90 -0
  240. package/src/contract.ts +181 -0
  241. package/src/currency-digits.ts +37 -0
  242. package/src/entitlement.ts +84 -0
  243. package/src/fields.ts +37 -0
  244. package/src/index.ts +163 -0
  245. package/src/invoice.ts +58 -0
  246. package/src/mailing/index.ts +11 -0
  247. package/src/mailing/reminder-mail.ts +108 -0
  248. package/src/manual.ts +31 -0
  249. package/src/messages/en.ts +150 -0
  250. package/src/messages/index.ts +18 -0
  251. package/src/messages/pl.ts +152 -0
  252. package/src/next/access.tsx +55 -0
  253. package/src/next/actions.ts +176 -0
  254. package/src/next/context.ts +18 -0
  255. package/src/next/current-entitlement.ts +36 -0
  256. package/src/next/index.ts +11 -0
  257. package/src/next/next-modules.d.ts +21 -0
  258. package/src/next/pages.tsx +267 -0
  259. package/src/next/pricing.tsx +39 -0
  260. package/src/next/route.ts +57 -0
  261. package/src/options.ts +128 -0
  262. package/src/payment.ts +77 -0
  263. package/src/plans.ts +63 -0
  264. package/src/price.ts +50 -0
  265. package/src/refund.ts +135 -0
  266. package/src/reminder.ts +52 -0
  267. package/src/schema.ts +86 -0
  268. package/src/scripts/entitlement-scripts.ts +188 -0
  269. package/src/scripts/index.ts +11 -0
  270. package/src/scripts/plan-scripts.ts +143 -0
  271. package/src/server/entitlements.ts +227 -0
  272. package/src/server/grants.ts +227 -0
  273. package/src/server/health.ts +18 -0
  274. package/src/server/index.ts +83 -0
  275. package/src/server/options.ts +52 -0
  276. package/src/server/payments.ts +434 -0
  277. package/src/server/plans.ts +143 -0
  278. package/src/server/privacy.ts +189 -0
  279. package/src/server/reminders.ts +113 -0
  280. package/src/server/requests.ts +201 -0
  281. package/src/server/setup.ts +37 -0
  282. package/src/server/take-back.ts +190 -0
  283. package/src/server/user-id.ts +12 -0
  284. package/src/stripe-currency.ts +65 -0
  285. package/src/stripe-webhook.ts +278 -0
  286. package/src/stripe.ts +130 -0
  287. package/src/ui/access-badge.tsx +64 -0
  288. package/src/ui/access-notice.tsx +73 -0
  289. package/src/ui/format.ts +25 -0
  290. package/src/ui/grant-form.tsx +69 -0
  291. package/src/ui/grant-history.tsx +127 -0
  292. package/src/ui/index.ts +25 -0
  293. package/src/ui/payment-form.tsx +111 -0
  294. package/src/ui/payment-requests.tsx +126 -0
  295. package/src/ui/pricing-tiles.tsx +105 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,OAAO,EAAE,SAAS,EAA6C,MAAM,kBAAkB,CAAC;AAKxF,MAAM,SAAS,GAAG,SAAS,CAAC;AAE5B,sGAAsG;AACtG,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,sGAAsG,CAAC,CAAC;IAC1H,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAqB;IACrD,qEAAqE;IACrE,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,OAAyB,CAAC;AAC5D,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,+EAA+E;IAC/E,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAoB,CAAC;AAC7E,CAAC;AASD,4DAA4D;AAC5D,MAAM,UAAU,gBAAgB,CAAC,MAAqB;IACpD,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IAC/C,MAAM,IAAI,GAAG,CAAC,IAAyB,EAAU,EAAE;QACjD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,wFAAwF;QACxF,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,IAAI,uCAAuC,CAAC,CAAC;QACpH,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IACF,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;AAC5D,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,oBAAoB,CAAC,MAAqB;IACxD,MAAM,OAAO,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,iBAAiB,EAAE,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,gBAAgB,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;AACnI,CAAC"}
@@ -0,0 +1,140 @@
1
+ import { type Err, type Ok } from "@softure-ai/core";
2
+ import type { Entitlement, PaymentGrant } from "../contract.js";
3
+ import { type FailedRefund, type PaidCheckout, type StripeWebhookError } from "../stripe-webhook.js";
4
+ import { type BillingContext } from "./entitlements.js";
5
+ /** The name of `stripe()`, under which its webhook stores payments. */
6
+ export declare const STRIPE_PROVIDER = "stripe";
7
+ export interface RecordPaymentInput extends PaidCheckout {
8
+ /** The adapter's name, e.g. `stripe`. */
9
+ readonly provider: string;
10
+ }
11
+ /** What a webhook delivery changed. */
12
+ export type PaymentOutcome =
13
+ /** A new paid checkout: its plan was granted. */
14
+ {
15
+ readonly status: "granted";
16
+ readonly entitlement: Entitlement;
17
+ }
18
+ /** A full refund of a recorded payment (or the partial one that completes it): what it granted was taken back. */
19
+ | {
20
+ readonly status: "refunded";
21
+ readonly entitlement: Entitlement;
22
+ }
23
+ /** A partial refund of a recorded payment: access changed by the `partialRefunds` policy. */
24
+ | {
25
+ readonly status: "partially_refunded";
26
+ readonly entitlement: Entitlement;
27
+ }
28
+ /** A refund of a recorded payment failed: what it took was given back (nothing when billing never counted it). */
29
+ | {
30
+ readonly status: "refund_failed";
31
+ readonly entitlement: Entitlement;
32
+ }
33
+ /** Recorded (or refunded) already: a repeated delivery, nothing changed. */
34
+ | {
35
+ readonly status: "duplicate";
36
+ }
37
+ /** A refund of a payment billing never recorded: nothing to take back. */
38
+ | {
39
+ readonly status: "unknown_payment";
40
+ }
41
+ /** An event billing does not act on. */
42
+ | {
43
+ readonly status: "ignored";
44
+ readonly reason: string;
45
+ };
46
+ export type RecordPaymentError = "billing.account_unknown" | "billing.plan_unknown";
47
+ /**
48
+ * Records a paid checkout and grants one payment of its plan, in one transaction: `duplicate` when
49
+ * the checkout was recorded before. An account or plan that no longer exists stores nothing (the
50
+ * money is refunded in the provider's dashboard). Database errors propagate.
51
+ */
52
+ export declare function recordPayment(ctx: BillingContext, input: RecordPaymentInput): Promise<Ok<PaymentOutcome> | Err<RecordPaymentError>>;
53
+ export interface RefundPaymentInput {
54
+ readonly provider: string;
55
+ /** The provider's payment id the refund names (a Stripe PaymentIntent). */
56
+ readonly paymentId: string;
57
+ /**
58
+ * For a partial refund, the total refunded so far in the currency's minor unit (Stripe's
59
+ * `amount_refunded`); omitted when the payment is refunded in full. A total that reaches the
60
+ * payment's amount is a full refund.
61
+ */
62
+ readonly amountRefunded?: number;
63
+ /**
64
+ * When the provider took the charge's state the delivery reports (Stripe's event `created`); now
65
+ * when omitted. Refunds that failed after it and were created before it are not counted in it.
66
+ */
67
+ readonly observedAt?: Date;
68
+ }
69
+ export interface GrantColumns {
70
+ readonly grantKind: "period" | "lifetime" | null;
71
+ readonly grantedFrom: Date | null;
72
+ readonly grantedUntil: Date | null;
73
+ }
74
+ /** The payment row's columns for what its grant added. */
75
+ export declare function getGrantColumns(grant: PaymentGrant): GrantColumns;
76
+ /** The grant a payment row records, or null for a row stored before grants were. */
77
+ export declare function readGrant(row: GrantColumns): PaymentGrant | null;
78
+ /**
79
+ * Records a refund of a payment and takes back what it granted, in one transaction. A full refund
80
+ * marks it refunded: a period loses its unused days, a lifetime ends unless another paid lifetime
81
+ * payment or an active manual lifetime grant still gives it, and a payment stored before grants were
82
+ * recorded revokes paid access. A partial refund keeps it paid and follows
83
+ * `billing({ partialRefunds })`: `pro_rata` takes back the share of the unused days that the newly
84
+ * refunded money is of the money not refunded before (rounded down) and shortens the payment's
85
+ * stored period by them; `keep_access` takes nothing back. Partial refunds never end a lifetime
86
+ * nor revoke a payment without a recorded grant; the refund that completes the amount does what a
87
+ * full refund does. The reported total first loses the failed refunds it still counts (created by
88
+ * `observedAt`, failed after it). `duplicate` when the refund adds nothing to what was recorded (a
89
+ * repeated or stale delivery); a state newer than every one recorded is then kept for the failure
90
+ * that explains it (`failRefund`). `unknown_payment` when billing never recorded the payment. The
91
+ * days a refund takes are added to the payment's `taken_back_days`, for a failure to give back.
92
+ * Database errors propagate.
93
+ */
94
+ export declare function refundPayment(ctx: BillingContext, input: RefundPaymentInput): Promise<Ok<PaymentOutcome>>;
95
+ export interface FailRefundInput extends FailedRefund {
96
+ /** The adapter's name, e.g. `stripe`. */
97
+ readonly provider: string;
98
+ }
99
+ /**
100
+ * Records a refund of a payment that failed and gives back what it took, in one transaction, under
101
+ * the locks of `refundPayment`. Billing counted the refund when the newest charge snapshot it
102
+ * recorded was taken after the refund was created and before it failed; then the payment's
103
+ * refunded total drops by the refund's amount, a payment refunded in full is paid again (a lifetime
104
+ * comes back), and its period gets back the failed money's share of the days refunds took
105
+ * (`getRestoredDays`, by `partialRefunds`), with `giveBackDays`. A failure billing never counted is
106
+ * only recorded, so a later snapshot that still counts the refund is corrected by it. A payment
107
+ * stored before grants were recorded gets its total and status back but no access. Then the charge
108
+ * state `refundPayment` kept is applied when, corrected, it reports more than billing now counts: a
109
+ * new refund reported before this failure is taken back once. `duplicate` for a refund already
110
+ * recorded as failed, `unknown_payment` when billing never recorded the payment. Database errors
111
+ * propagate.
112
+ */
113
+ export declare function failRefund(ctx: BillingContext, input: FailRefundInput): Promise<Ok<PaymentOutcome>>;
114
+ export interface ReceiveStripeWebhookInput {
115
+ /** The raw request body, exactly as Stripe sent it. */
116
+ readonly payload: string;
117
+ /** The `Stripe-Signature` header, or null. */
118
+ readonly signature: string | null;
119
+ /** The endpoint's signing secret (`whsec_...`). */
120
+ readonly secret: string;
121
+ }
122
+ export interface StripeWebhookReceipt {
123
+ readonly eventId: string;
124
+ readonly outcome: PaymentOutcome
125
+ /** A paid checkout whose account or plan is gone: nothing stored, refund it in Stripe. */
126
+ | {
127
+ readonly status: "refused";
128
+ readonly error: RecordPaymentError;
129
+ readonly checkoutId: string;
130
+ };
131
+ }
132
+ /**
133
+ * One Stripe webhook delivery: the signature is checked before anything is parsed or read, then a
134
+ * paid checkout is recorded and granted, a refund takes back what its payment granted (a partial
135
+ * one by the `partialRefunds` policy), and a failed refund gives it back.
136
+ * `billing.webhook_invalid` for a delivery that is not Stripe's (or a replay past the tolerance).
137
+ * Database errors propagate (answer 500, Stripe retries).
138
+ */
139
+ export declare function receiveStripeWebhook(ctx: BillingContext, input: ReceiveStripeWebhookInput): Promise<Ok<StripeWebhookReceipt> | Err<StripeWebhookError>>;
140
+ //# sourceMappingURL=payments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payments.d.ts","sourceRoot":"","sources":["../../src/server/payments.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAW,KAAK,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAG9D,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKhE,OAAO,EAAqB,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACxH,OAAO,EAA4C,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAMlG,uEAAuE;AACvE,eAAO,MAAM,eAAe,WAAW,CAAC;AAExC,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,uCAAuC;AACvC,MAAM,MAAM,cAAc;AACxB,iDAAiD;AAC/C;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;CAAE;AACnE,kHAAkH;GAChH;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;CAAE;AACpE,6FAA6F;GAC3F;IAAE,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;CAAE;AAC9E,kHAAkH;GAChH;IAAE,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;CAAE;AACzE,4EAA4E;GAC1E;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE;AAClC,0EAA0E;GACxE;IAAE,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE;AACxC,wCAAwC;GACtC;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,MAAM,MAAM,kBAAkB,GAAG,yBAAyB,GAAG,sBAAsB,CAAC;AAEpF;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,EAAE,CAAC,cAAc,CAAC,GAAG,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAqCzI;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;CACpC;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAGjE;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,GAAG,EAAE,YAAY,GAAG,YAAY,GAAG,IAAI,CAIhE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAoB/G;AAqFD,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAoCzG;AAoED,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EACZ,cAAc;IAChB,0FAA0F;OACxF;QAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CACrG;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,yBAAyB,GAAG,OAAO,CAAC,EAAE,CAAC,oBAAoB,CAAC,GAAG,GAAG,CAAC,kBAAkB,CAAC,CAAC,CA2B7J"}
@@ -0,0 +1,339 @@
1
+ // Payments a provider reports through its webhook: a paid checkout grants its plan and a refund
2
+ // takes back what that payment granted (a partial one by the `partialRefunds` policy), each exactly
3
+ // once. `billing.payments` holds one row per paid checkout with the grant it caused (a period or
4
+ // lifetime), written in the transaction of the grant, so a delivery Stripe repeats (or two events
5
+ // for one checkout) finds the row and changes nothing.
6
+ // A payment keeps the total refunded so far (`refunded_amount`, the provider's cumulative figure),
7
+ // so a repeated or stale refund delivery finds nothing new and changes nothing. A refund that fails
8
+ // later gives back what it took (`failRefund`), once per refund id (`billing.refund_failures`); a
9
+ // charge snapshot taken before a failure is corrected by the failed refunds it still counts. A newer
10
+ // snapshot that reports no more than billing counts (a new refund after a failure billing has not
11
+ // heard of yet) is kept on the payment, and each failure applies it once it reports more.
12
+ // Locks: the account first (key share), like `changeEntitlement` and the privacy erase; a refund
13
+ // then takes the entitlement before its payment row (`lockEntitlementRow`), as a manual revoke does,
14
+ // so a refund and a revoke of one account queue on the entitlement instead of deadlocking on the
15
+ // rows each moves back.
16
+ import { users } from "@softure-ai/auth";
17
+ import { err, ok } from "@softure-ai/core";
18
+ import { and, eq, gt, lte, sql } from "drizzle-orm";
19
+ import { findPlan } from "../plans.js";
20
+ import { resolveEntitlement } from "../entitlement.js";
21
+ import { getRestoredDays, moveBackByDays } from "../refund.js";
22
+ import { payments, refundFailures } from "../schema.js";
23
+ import { readStripeWebhook } from "../stripe-webhook.js";
24
+ import { changeEntitlement, findEntitlementRecord } from "./entitlements.js";
25
+ import { getBillingOptions, getEntitlementPolicy } from "./options.js";
26
+ import { applyPlan, getBillingPlans } from "./plans.js";
27
+ import { giveBackDays, hasActiveManualLifetime, hasPaidLifetimePayment, lockEntitlementRow, takeBackGrant } from "./take-back.js";
28
+ import { isUserId } from "./user-id.js";
29
+ /** The name of `stripe()`, under which its webhook stores payments. */
30
+ export const STRIPE_PROVIDER = "stripe";
31
+ /**
32
+ * Records a paid checkout and grants one payment of its plan, in one transaction: `duplicate` when
33
+ * the checkout was recorded before. An account or plan that no longer exists stores nothing (the
34
+ * money is refunded in the provider's dashboard). Database errors propagate.
35
+ */
36
+ export async function recordPayment(ctx, input) {
37
+ if (findPlan(getBillingPlans(ctx.config), input.planId) === undefined)
38
+ return err("billing.plan_unknown");
39
+ if (!isUserId(input.userId))
40
+ return err("billing.account_unknown");
41
+ return ctx.db.transaction(async (tx) => {
42
+ const now = ctx.clock.now();
43
+ // A shared lock: the account cannot be deleted before the grant below.
44
+ const [account] = await tx.select({ id: users.id }).from(users).where(eq(users.id, input.userId)).for("key share");
45
+ if (account === undefined)
46
+ return err("billing.account_unknown");
47
+ // A concurrent delivery of the same checkout waits here for this transaction, then conflicts.
48
+ const inserted = await tx
49
+ .insert(payments)
50
+ .values({
51
+ userId: input.userId,
52
+ provider: input.provider,
53
+ checkoutId: input.checkoutId,
54
+ paymentId: input.paymentId,
55
+ planId: input.planId,
56
+ amount: input.amount,
57
+ currency: input.currency,
58
+ status: "paid",
59
+ paidAt: now,
60
+ })
61
+ .onConflictDoNothing()
62
+ .returning();
63
+ if (inserted.length === 0)
64
+ return ok({ status: "duplicate" });
65
+ const applied = await applyPlan({ ...ctx, db: tx }, input.userId, input.planId);
66
+ // The plan and the locked account were checked above, and a plan grant always ends later.
67
+ if (!applied.ok)
68
+ throw new Error(`@softure-ai/billing: granting the paid plan "${input.planId}" failed with ${applied.error}`);
69
+ const [payment] = inserted;
70
+ const { grant } = applied.value;
71
+ // A plan grant always adds a period of at least a day, or lifetime access.
72
+ if (payment === undefined || grant === null)
73
+ throw new Error(`@softure-ai/billing: the paid plan "${input.planId}" was granted but its payment row or grant is missing`);
74
+ await tx.update(payments).set(getGrantColumns(grant)).where(eq(payments.id, payment.id));
75
+ return ok({ status: "granted", entitlement: applied.value.entitlement });
76
+ });
77
+ }
78
+ /** The payment row's columns for what its grant added. */
79
+ export function getGrantColumns(grant) {
80
+ if (grant.kind === "lifetime")
81
+ return { grantKind: "lifetime", grantedFrom: null, grantedUntil: null };
82
+ return { grantKind: "period", grantedFrom: grant.from, grantedUntil: grant.until };
83
+ }
84
+ /** The grant a payment row records, or null for a row stored before grants were. */
85
+ export function readGrant(row) {
86
+ if (row.grantKind === "lifetime")
87
+ return { kind: "lifetime" };
88
+ if (row.grantKind === "period" && row.grantedFrom !== null && row.grantedUntil !== null)
89
+ return { kind: "period", from: row.grantedFrom, until: row.grantedUntil };
90
+ return null;
91
+ }
92
+ /**
93
+ * Records a refund of a payment and takes back what it granted, in one transaction. A full refund
94
+ * marks it refunded: a period loses its unused days, a lifetime ends unless another paid lifetime
95
+ * payment or an active manual lifetime grant still gives it, and a payment stored before grants were
96
+ * recorded revokes paid access. A partial refund keeps it paid and follows
97
+ * `billing({ partialRefunds })`: `pro_rata` takes back the share of the unused days that the newly
98
+ * refunded money is of the money not refunded before (rounded down) and shortens the payment's
99
+ * stored period by them; `keep_access` takes nothing back. Partial refunds never end a lifetime
100
+ * nor revoke a payment without a recorded grant; the refund that completes the amount does what a
101
+ * full refund does. The reported total first loses the failed refunds it still counts (created by
102
+ * `observedAt`, failed after it). `duplicate` when the refund adds nothing to what was recorded (a
103
+ * repeated or stale delivery); a state newer than every one recorded is then kept for the failure
104
+ * that explains it (`failRefund`). `unknown_payment` when billing never recorded the payment. The
105
+ * days a refund takes are added to the payment's `taken_back_days`, for a failure to give back.
106
+ * Database errors propagate.
107
+ */
108
+ export async function refundPayment(ctx, input) {
109
+ return ctx.db.transaction(async (tx) => {
110
+ const now = ctx.clock.now();
111
+ const match = and(eq(payments.provider, input.provider), eq(payments.paymentId, input.paymentId));
112
+ const [found] = await tx.select({ userId: payments.userId }).from(payments).where(match);
113
+ if (found === undefined)
114
+ return ok({ status: "unknown_payment" });
115
+ const { userId } = found;
116
+ // The account first (the lock order of every change), the entitlement, then the payment row.
117
+ await tx.select({ id: users.id }).from(users).where(eq(users.id, userId)).for("key share");
118
+ await lockEntitlementRow(tx, userId);
119
+ const [payment] = await tx.select().from(payments).where(match).for("update");
120
+ // Erased with the account in the meantime.
121
+ if (payment === undefined)
122
+ return ok({ status: "duplicate" });
123
+ const state = { reported: input.amountRefunded ?? payment.amount, observedAt: input.observedAt ?? now };
124
+ const applied = await applyChargeState({ ...ctx, db: tx }, { payment, state, now });
125
+ if (applied !== null)
126
+ return ok(applied);
127
+ await keepNewerChargeState(tx, payment, state);
128
+ return ok({ status: "duplicate" });
129
+ });
130
+ }
131
+ /**
132
+ * Applies a charge state to a payment the caller locked (the account, the entitlement, the row):
133
+ * the refunded total it reports, less the failed refunds it still counts, takes back what it adds
134
+ * to the recorded total (see `refundPayment`). Null when it adds nothing (the payment is refunded
135
+ * in full, or the total is not above the recorded one). A kept state at or before it is cleared.
136
+ */
137
+ async function applyChargeState(ctx, { payment, state, now }) {
138
+ const total = Math.min(Math.max(state.reported - (await getFailedAmountCounted(ctx.db, payment.id, state.observedAt)), 0), payment.amount);
139
+ const isFull = total >= payment.amount;
140
+ if (payment.status !== "paid" || (!isFull && total <= payment.refundedAmount))
141
+ return null;
142
+ const { userId } = payment;
143
+ const refundsSeenAt = payment.refundsSeenAt !== null && payment.refundsSeenAt > state.observedAt ? payment.refundsSeenAt : state.observedAt;
144
+ const isPendingIncluded = payment.pendingRefundsSeenAt !== null && payment.pendingRefundsSeenAt <= state.observedAt;
145
+ await ctx.db
146
+ .update(payments)
147
+ .set({
148
+ ...(isFull ? { status: "refunded", refundedAt: now, refundedAmount: payment.amount } : { refundedAmount: total }),
149
+ refundsSeenAt,
150
+ ...(isPendingIncluded ? { pendingRefundedAmount: null, pendingRefundsSeenAt: null } : {}),
151
+ })
152
+ .where(eq(payments.id, payment.id));
153
+ const share = isFull ? undefined : getPartialShare(ctx, { refunded: total - payment.refundedAmount, outstanding: payment.amount - payment.refundedAmount });
154
+ const grant = readGrant(payment);
155
+ const { entitlement, days } = await takeBackGrant(ctx, {
156
+ userId,
157
+ grant,
158
+ now,
159
+ share,
160
+ hasOtherLifetime: async () => (await hasActiveManualLifetime(ctx.db, userId)) || (await hasPaidLifetimePayment(ctx.db, userId, payment.id)),
161
+ });
162
+ if (days > 0 && grant?.kind === "period") {
163
+ // A partially refunded payment stays paid: its stored period ends where the access it still pays for does.
164
+ const grantedUntil = isFull ? grant.until : moveBackByDays(grant.until, days, ctx.config.timezone);
165
+ await ctx.db
166
+ .update(payments)
167
+ .set({ grantedUntil, takenBackDays: payment.takenBackDays + days })
168
+ .where(eq(payments.id, payment.id));
169
+ }
170
+ return { status: isFull ? "refunded" : "partially_refunded", entitlement };
171
+ }
172
+ /**
173
+ * Keeps a charge state billing did not apply when it is newer than every state recorded (applied or
174
+ * kept): it may carry a new refund that a failure billing has not heard of yet hides.
175
+ */
176
+ async function keepNewerChargeState(tx, payment, state) {
177
+ const isNewer = (seenAt) => seenAt === null || state.observedAt > seenAt;
178
+ if (!isNewer(payment.refundsSeenAt) || !isNewer(payment.pendingRefundsSeenAt))
179
+ return;
180
+ await tx
181
+ .update(payments)
182
+ .set({ pendingRefundedAmount: state.reported, pendingRefundsSeenAt: state.observedAt })
183
+ .where(eq(payments.id, payment.id));
184
+ }
185
+ /** The failed refunds of a payment a charge snapshot taken at `observedAt` still counts: created by then, failed after. */
186
+ async function getFailedAmountCounted(tx, paymentId, observedAt) {
187
+ const [row] = await tx
188
+ .select({ amount: sql `coalesce(sum(${refundFailures.amount}), 0)` })
189
+ .from(refundFailures)
190
+ .where(and(eq(refundFailures.paymentId, paymentId), lte(refundFailures.refundCreatedAt, observedAt), gt(refundFailures.failedAt, observedAt)));
191
+ return Number(row?.amount ?? 0);
192
+ }
193
+ /**
194
+ * Records a refund of a payment that failed and gives back what it took, in one transaction, under
195
+ * the locks of `refundPayment`. Billing counted the refund when the newest charge snapshot it
196
+ * recorded was taken after the refund was created and before it failed; then the payment's
197
+ * refunded total drops by the refund's amount, a payment refunded in full is paid again (a lifetime
198
+ * comes back), and its period gets back the failed money's share of the days refunds took
199
+ * (`getRestoredDays`, by `partialRefunds`), with `giveBackDays`. A failure billing never counted is
200
+ * only recorded, so a later snapshot that still counts the refund is corrected by it. A payment
201
+ * stored before grants were recorded gets its total and status back but no access. Then the charge
202
+ * state `refundPayment` kept is applied when, corrected, it reports more than billing now counts: a
203
+ * new refund reported before this failure is taken back once. `duplicate` for a refund already
204
+ * recorded as failed, `unknown_payment` when billing never recorded the payment. Database errors
205
+ * propagate.
206
+ */
207
+ export async function failRefund(ctx, input) {
208
+ return ctx.db.transaction(async (tx) => {
209
+ const now = ctx.clock.now();
210
+ const txCtx = { ...ctx, db: tx };
211
+ const match = and(eq(payments.provider, input.provider), eq(payments.paymentId, input.paymentId));
212
+ const [found] = await tx.select({ userId: payments.userId }).from(payments).where(match);
213
+ if (found === undefined)
214
+ return ok({ status: "unknown_payment" });
215
+ const { userId } = found;
216
+ // The lock order of `refundPayment`: the account, the entitlement, then the payment row.
217
+ await tx.select({ id: users.id }).from(users).where(eq(users.id, userId)).for("key share");
218
+ await lockEntitlementRow(tx, userId);
219
+ const [payment] = await tx.select().from(payments).where(match).for("update");
220
+ // Erased with the account in the meantime.
221
+ if (payment === undefined)
222
+ return ok({ status: "unknown_payment" });
223
+ const recorded = await tx
224
+ .insert(refundFailures)
225
+ .values({
226
+ paymentId: payment.id,
227
+ refundId: input.refundId,
228
+ amount: input.amount,
229
+ refundCreatedAt: input.refundCreatedAt,
230
+ failedAt: input.failedAt,
231
+ recordedAt: now,
232
+ })
233
+ .onConflictDoNothing()
234
+ .returning();
235
+ if (recorded.length === 0)
236
+ return ok({ status: "duplicate" });
237
+ const seenAt = payment.refundsSeenAt;
238
+ const isCounted = seenAt !== null && input.refundCreatedAt <= seenAt && seenAt < input.failedAt;
239
+ const restoredAmount = isCounted ? Math.min(input.amount, payment.refundedAmount) : 0;
240
+ const restored = restoredAmount === 0 ? null : await restoreRefund(txCtx, { payment, restoredAmount, now });
241
+ const kept = await applyKeptChargeState(txCtx, payment.id, now);
242
+ return ok({ status: "refund_failed", entitlement: kept?.entitlement ?? restored ?? (await readEntitlement(txCtx, userId, now)) });
243
+ });
244
+ }
245
+ /** Gives back what a failed refund billing counted took (see `failRefund`); the entitlement when access changed. */
246
+ async function restoreRefund(ctx, { payment, restoredAmount, now }) {
247
+ const { userId } = payment;
248
+ const days = getRestoredDays({
249
+ takenBackDays: payment.takenBackDays,
250
+ refundedAmount: payment.refundedAmount,
251
+ restoredAmount,
252
+ policy: getBillingOptions(ctx.config).partialRefunds,
253
+ });
254
+ const isPaid = payment.status === "paid";
255
+ const grant = readGrant(payment);
256
+ let entitlement = null;
257
+ let period = null;
258
+ if (grant?.kind === "period" && days > 0) {
259
+ const given = await giveBackDays(ctx, { userId, grant, isPaid, days, now });
260
+ entitlement = given.entitlement;
261
+ period = getGrantColumns(given.period);
262
+ }
263
+ else if (grant?.kind === "lifetime" && !isPaid) {
264
+ const changed = await changeEntitlement(ctx, userId, { type: "grant_lifetime" });
265
+ // The account is locked and a lifetime grant is never refused.
266
+ if (!changed.ok)
267
+ throw new Error(`@softure-ai/billing: giving back a refunded lifetime failed with ${changed.error}`);
268
+ entitlement = changed.value;
269
+ }
270
+ // Below the amount again, so a payment refunded in full is paid again.
271
+ await ctx.db
272
+ .update(payments)
273
+ .set({
274
+ status: "paid",
275
+ refundedAt: null,
276
+ refundedAmount: payment.refundedAmount - restoredAmount,
277
+ takenBackDays: payment.takenBackDays - days,
278
+ ...(period === null ? {} : { grantedFrom: period.grantedFrom, grantedUntil: period.grantedUntil }),
279
+ })
280
+ .where(eq(payments.id, payment.id));
281
+ return entitlement;
282
+ }
283
+ /** Applies the charge state a payment kept (see `refundPayment`) when it now reports more than billing counts. */
284
+ async function applyKeptChargeState(ctx, paymentId, now) {
285
+ // Read again: the restore may have changed the row. The caller holds its lock.
286
+ const [payment] = await ctx.db.select().from(payments).where(eq(payments.id, paymentId));
287
+ if (payment === undefined || payment.pendingRefundedAmount === null || payment.pendingRefundsSeenAt === null)
288
+ return null;
289
+ return applyChargeState(ctx, { payment, state: { reported: payment.pendingRefundedAmount, observedAt: payment.pendingRefundsSeenAt }, now });
290
+ }
291
+ /** Where the account stands at `now`, read inside the caller's transaction. */
292
+ async function readEntitlement(ctx, userId, now) {
293
+ const record = await findEntitlementRecord(ctx, userId);
294
+ // The caller's payment row references the account, which its key share lock keeps.
295
+ if (record === null)
296
+ throw new Error("@softure-ai/billing: a refunded payment has no account");
297
+ return resolveEntitlement(record, now, getEntitlementPolicy(ctx.config));
298
+ }
299
+ /** The share a partial refund takes back under the app's policy: none under `keep_access`. */
300
+ function getPartialShare(ctx, share) {
301
+ return getBillingOptions(ctx.config).partialRefunds === "keep_access" ? { ...share, refunded: 0 } : share;
302
+ }
303
+ /**
304
+ * One Stripe webhook delivery: the signature is checked before anything is parsed or read, then a
305
+ * paid checkout is recorded and granted, a refund takes back what its payment granted (a partial
306
+ * one by the `partialRefunds` policy), and a failed refund gives it back.
307
+ * `billing.webhook_invalid` for a delivery that is not Stripe's (or a replay past the tolerance).
308
+ * Database errors propagate (answer 500, Stripe retries).
309
+ */
310
+ export async function receiveStripeWebhook(ctx, input) {
311
+ const event = readStripeWebhook({ payload: input.payload, header: input.signature, secret: input.secret, now: ctx.clock.now() });
312
+ if (!event.ok)
313
+ return event;
314
+ const { eventId } = event.value;
315
+ switch (event.value.type) {
316
+ case "ignored":
317
+ return ok({ eventId, outcome: { status: "ignored", reason: event.value.reason } });
318
+ case "checkout_paid": {
319
+ const { checkout } = event.value;
320
+ const recorded = await recordPayment(ctx, { provider: STRIPE_PROVIDER, ...checkout });
321
+ return ok({ eventId, outcome: recorded.ok ? recorded.value : { status: "refused", error: recorded.error, checkoutId: checkout.checkoutId } });
322
+ }
323
+ case "payment_refunded": {
324
+ const { paymentId, snapshotAt } = event.value;
325
+ const refunded = await refundPayment(ctx, { provider: STRIPE_PROVIDER, paymentId, observedAt: snapshotAt });
326
+ return ok({ eventId, outcome: refunded.value });
327
+ }
328
+ case "payment_partially_refunded": {
329
+ const { paymentId, amountRefunded, snapshotAt } = event.value;
330
+ const refunded = await refundPayment(ctx, { provider: STRIPE_PROVIDER, paymentId, amountRefunded, observedAt: snapshotAt });
331
+ return ok({ eventId, outcome: refunded.value });
332
+ }
333
+ case "refund_failed": {
334
+ const failed = await failRefund(ctx, { provider: STRIPE_PROVIDER, ...event.value.failure });
335
+ return ok({ eventId, outcome: failed.value });
336
+ }
337
+ }
338
+ }
339
+ //# sourceMappingURL=payments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payments.js","sourceRoot":"","sources":["../../src/server/payments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { type Err, type Ok, type SoftureConfig } from "@softure-ai/core";
2
+ import type { BillingErrorCode, Entitlement, PaymentErrorCode, PaymentGrant, Plan } from "../contract.js";
3
+ import { type InvoiceDetailsError, type InvoiceInput } from "../invoice.js";
4
+ import type { PaymentAccount, PaymentProvider, PaymentStart } from "../payment.js";
5
+ import { type BillingContext } from "./entitlements.js";
6
+ /** The plans of `billing({ plans })`, in their order. */
7
+ export declare function getBillingPlans(config: SoftureConfig): readonly Plan[];
8
+ /** The provider of `billing({ payment })`. Throws when the app set none: a payment page without one is a bug. */
9
+ export declare function getPaymentProvider(config: SoftureConfig): PaymentProvider;
10
+ /** The account with this email (compared trimmed and lower-cased, as auth stores it), or null. */
11
+ export declare function findAccountByEmail(ctx: Pick<BillingContext, "db">, email: string): Promise<PaymentAccount | null>;
12
+ /** The account with this id, or null (also for an id that is not a uuid). */
13
+ export declare function findAccountById(ctx: Pick<BillingContext, "db">, id: string): Promise<PaymentAccount | null>;
14
+ /** Where the account stands after a plan grant, and what the grant added (null when it added nothing). */
15
+ export interface AppliedPlan {
16
+ readonly entitlement: Entitlement;
17
+ readonly grant: PaymentGrant | null;
18
+ }
19
+ /**
20
+ * `grantPlan` that also says what the grant added, for the payment row that pays for it. The event
21
+ * is computed under the entitlement's lock and may be computed twice (a concurrent first change), so
22
+ * the grant is taken from the last computation, the one applied.
23
+ */
24
+ export declare function applyPlan(ctx: BillingContext, userId: string, planId: string): Promise<Ok<AppliedPlan> | Err<BillingErrorCode | "billing.plan_unknown">>;
25
+ /**
26
+ * Grants one payment of the plan: a paid period that starts when the account's current access ends,
27
+ * or lifetime access. Computed under the entitlement's lock, so two grants at once give two periods.
28
+ * It records nothing: a grant made here is not in the account's history and cannot be revoked; an
29
+ * admin's grant goes through `grantPlanManually`.
30
+ */
31
+ export declare function grantPlan(ctx: BillingContext, userId: string, planId: string): Promise<Ok<Entitlement> | Err<BillingErrorCode | "billing.plan_unknown">>;
32
+ export interface StartPaymentInput {
33
+ readonly account: PaymentAccount;
34
+ readonly planId: string;
35
+ /** What the form sent; ignored when the provider collects its own details. */
36
+ readonly invoice: InvoiceInput;
37
+ }
38
+ export type StartPaymentResult = Ok<PaymentStart> | Err<Exclude<PaymentErrorCode, "billing.invoice_details_invalid"> | "security.rate_limited"> | InvoiceDetailsError;
39
+ /**
40
+ * Starts paying for a plan: counts `billing-payment` per account first, checks the plan, that the
41
+ * account has no lifetime access yet and, for a provider that needs them, the invoice details, then
42
+ * starts the provider. A provider that hands requests over (`handsOverRequests`, the manual adapter)
43
+ * gets the request stored first and is called once per open request: asking again refreshes the
44
+ * stored request and answers `requested` without a second hand-over, and a hand-over that failed
45
+ * (an `Err` or a throw) is released, so the next ask tries again; a claim left without an answer (the
46
+ * process stopped) is taken over by an ask a minute later. Database errors and provider
47
+ * throws propagate; a provider whose answer contradicts `handsOverRequests` throws.
48
+ */
49
+ export declare function startPayment(ctx: BillingContext, input: StartPaymentInput): Promise<StartPaymentResult>;
50
+ //# sourceMappingURL=plans.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../../src/server/plans.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,GAAG,EAAE,KAAK,EAAE,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGlF,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAoB,gBAAgB,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC5H,OAAO,EAAuB,KAAK,mBAAmB,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AACjG,OAAO,KAAK,EAAkB,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGnG,OAAO,EAA4C,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAMlG,yDAAyD;AACzD,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,IAAI,EAAE,CAEtE;AAED,iHAAiH;AACjH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAIzE;AAED,kGAAkG;AAClG,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAOvH;AAED,6EAA6E;AAC7E,wBAAsB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAIjH;AAED,0GAA0G;AAC1G,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACrC;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,GAAG,CAAC,gBAAgB,GAAG,sBAAsB,CAAC,CAAC,CAU9J;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,GAAG,GAAG,CAAC,gBAAgB,GAAG,sBAAsB,CAAC,CAAC,CAG9J;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;CAChC;AAED,MAAM,MAAM,kBAAkB,GAAG,EAAE,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,EAAE,iCAAiC,CAAC,GAAG,uBAAuB,CAAC,GAAG,mBAAmB,CAAC;AAEtK;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA2C7G"}