@carlonicora/nextjs-jsonapi 5.5.0 → 5.7.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 (152) hide show
  1. package/dist/{BlockNoteEditor-GVQCKRMU.mjs → BlockNoteEditor-R3WXUFHX.mjs} +4 -4
  2. package/dist/{BlockNoteEditor-THBJH2UR.js → BlockNoteEditor-ZHXXFWUJ.js} +19 -19
  3. package/dist/{BlockNoteEditor-THBJH2UR.js.map → BlockNoteEditor-ZHXXFWUJ.js.map} +1 -1
  4. package/dist/{BlockNoteViewer-U6E3DS4F.mjs → BlockNoteViewer-BVVN6D3T.mjs} +2 -2
  5. package/dist/{BlockNoteViewer-VZOVKTFO.js → BlockNoteViewer-SD3ZFIXD.js} +3 -3
  6. package/dist/{BlockNoteViewer-VZOVKTFO.js.map → BlockNoteViewer-SD3ZFIXD.js.map} +1 -1
  7. package/dist/billing/index.d.mts +197 -46
  8. package/dist/billing/index.d.ts +197 -46
  9. package/dist/billing/index.js +1166 -589
  10. package/dist/billing/index.js.map +1 -1
  11. package/dist/billing/index.mjs +803 -226
  12. package/dist/billing/index.mjs.map +1 -1
  13. package/dist/{chunk-WPYT4UKJ.js → chunk-2TXE57LE.js} +3 -3
  14. package/dist/{chunk-WPYT4UKJ.js.map → chunk-2TXE57LE.js.map} +1 -1
  15. package/dist/{chunk-G6UE6MEP.js → chunk-5TH2DSAH.js} +901 -204
  16. package/dist/chunk-5TH2DSAH.js.map +1 -0
  17. package/dist/{chunk-X5WIUUNG.mjs → chunk-CFB6FHIY.mjs} +3342 -2377
  18. package/dist/chunk-CFB6FHIY.mjs.map +1 -0
  19. package/dist/{chunk-VC5A3AUZ.mjs → chunk-FK26NX3S.mjs} +701 -4
  20. package/dist/chunk-FK26NX3S.mjs.map +1 -0
  21. package/dist/{chunk-YDYVJM4I.js → chunk-IJME5SAM.js} +4 -4
  22. package/dist/{chunk-YDYVJM4I.js.map → chunk-IJME5SAM.js.map} +1 -1
  23. package/dist/{chunk-W7F5BDSV.mjs → chunk-OKCV4GPJ.mjs} +2 -2
  24. package/dist/{chunk-3CGROPF3.mjs → chunk-RGQYFSJF.mjs} +2 -2
  25. package/dist/{chunk-6SQYXUVP.mjs → chunk-WCHLCOZG.mjs} +2 -2
  26. package/dist/{chunk-S4YJ5WCW.js → chunk-XOSC3FFP.js} +8 -8
  27. package/dist/{chunk-S4YJ5WCW.js.map → chunk-XOSC3FFP.js.map} +1 -1
  28. package/dist/{chunk-TY5FYP3G.js → chunk-ZCOPOO7T.js} +2024 -1059
  29. package/dist/chunk-ZCOPOO7T.js.map +1 -0
  30. package/dist/client/index.js +4 -4
  31. package/dist/client/index.mjs +3 -3
  32. package/dist/components/index.js +4 -4
  33. package/dist/components/index.mjs +3 -3
  34. package/dist/contexts/index.js +4 -4
  35. package/dist/contexts/index.mjs +3 -3
  36. package/dist/core/index.d.mts +248 -3
  37. package/dist/core/index.d.ts +248 -3
  38. package/dist/core/index.js +42 -2
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/index.mjs +41 -1
  41. package/dist/{stripe-promotion-code.interface-ClZ7DxS9.d.mts → currency-DT_dveBx.d.mts} +137 -1
  42. package/dist/{stripe-promotion-code.interface-C3qqh3mi.d.ts → currency-pK9eRYRC.d.ts} +137 -1
  43. package/dist/email-automations/index.js +92 -92
  44. package/dist/email-automations/index.mjs +3 -3
  45. package/dist/features/analytics/index.js +121 -121
  46. package/dist/features/analytics/index.mjs +4 -4
  47. package/dist/features/analytics/server.js +4 -4
  48. package/dist/features/analytics/server.mjs +2 -2
  49. package/dist/features/blog/index.js +150 -150
  50. package/dist/features/blog/index.mjs +3 -3
  51. package/dist/features/blog/server.js +16 -16
  52. package/dist/features/blog/server.mjs +1 -1
  53. package/dist/features/help/index.js +37 -37
  54. package/dist/features/help/index.mjs +3 -3
  55. package/dist/features/tokenusage/index.js +124 -124
  56. package/dist/features/tokenusage/index.mjs +4 -4
  57. package/dist/index.d.mts +2 -2
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +43 -3
  60. package/dist/index.js.map +1 -1
  61. package/dist/index.mjs +42 -2
  62. package/dist/server/index.js +3 -3
  63. package/dist/server/index.mjs +2 -2
  64. package/messages/en.json +125 -3
  65. package/messages/it.json +125 -3
  66. package/package.json +1 -1
  67. package/scripts/validate-messages.mjs +1 -0
  68. package/src/billing/index.ts +1 -0
  69. package/src/client/index.ts +4 -0
  70. package/src/core/index.ts +4 -0
  71. package/src/core/registry/ModuleRegistry.ts +3 -0
  72. package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +3 -2
  73. package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +4 -3
  74. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +22 -5
  75. package/src/features/billing/components/utils/__tests__/discount.spec.ts +59 -0
  76. package/src/features/billing/components/utils/__tests__/url.spec.ts +32 -0
  77. package/src/features/billing/components/utils/discount.ts +38 -0
  78. package/src/features/billing/components/utils/index.ts +2 -0
  79. package/src/features/billing/components/utils/url.ts +12 -0
  80. package/src/features/billing/contexts/CouponContext.tsx +209 -0
  81. package/src/features/billing/contexts/index.ts +1 -0
  82. package/src/features/billing/i18n-keys.ts +79 -0
  83. package/src/features/billing/stripe-coupon/components/containers/CouponContainer.tsx +38 -0
  84. package/src/features/billing/stripe-coupon/components/containers/index.ts +1 -0
  85. package/src/features/billing/stripe-coupon/components/details/CouponDetails.tsx +72 -0
  86. package/src/features/billing/stripe-coupon/components/details/index.ts +1 -0
  87. package/src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx +80 -0
  88. package/src/features/billing/stripe-coupon/components/forms/CouponCodeFields.tsx +214 -0
  89. package/src/features/billing/stripe-coupon/components/forms/CouponEditor.tsx +425 -0
  90. package/src/features/billing/stripe-coupon/components/forms/CouponRefresher.tsx +45 -0
  91. package/src/features/billing/stripe-coupon/components/forms/index.ts +6 -0
  92. package/src/features/billing/stripe-coupon/components/index.ts +5 -0
  93. package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +61 -0
  94. package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +67 -0
  95. package/src/features/billing/stripe-coupon/components/lists/index.ts +2 -0
  96. package/src/features/billing/stripe-coupon/components/widgets/CouponCodeActions.tsx +81 -0
  97. package/src/features/billing/stripe-coupon/components/widgets/index.ts +1 -0
  98. package/src/features/billing/stripe-coupon/data/__tests__/stripe-coupon-models.spec.ts +114 -0
  99. package/src/features/billing/stripe-coupon/data/index.ts +10 -0
  100. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.fields.ts +8 -0
  101. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.interface.ts +44 -0
  102. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +92 -0
  103. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.ts +132 -0
  104. package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.interface.ts +17 -0
  105. package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.ts +70 -0
  106. package/src/features/billing/stripe-coupon/data/stripe-coupon.fields.ts +7 -0
  107. package/src/features/billing/stripe-coupon/data/stripe-coupon.interface.ts +55 -0
  108. package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +93 -0
  109. package/src/features/billing/stripe-coupon/data/stripe-coupon.ts +167 -0
  110. package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +161 -0
  111. package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +113 -0
  112. package/src/features/billing/stripe-coupon/index.ts +5 -0
  113. package/src/features/billing/stripe-coupon/stripe-coupon-code.module.ts +15 -0
  114. package/src/features/billing/stripe-coupon/stripe-coupon-offer.module.ts +10 -0
  115. package/src/features/billing/stripe-coupon/stripe-coupon.module.ts +25 -0
  116. package/src/features/billing/stripe-coupon/utils/__tests__/coupon-places.spec.ts +19 -0
  117. package/src/features/billing/stripe-coupon/utils/__tests__/to-promotion-code-result.spec.ts +41 -0
  118. package/src/features/billing/stripe-coupon/utils/coupon-places.ts +23 -0
  119. package/src/features/billing/stripe-coupon/utils/format-coupon-discount.ts +36 -0
  120. package/src/features/billing/stripe-coupon/utils/index.ts +3 -0
  121. package/src/features/billing/stripe-coupon/utils/to-promotion-code-result.ts +23 -0
  122. package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +24 -8
  123. package/src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx +21 -6
  124. package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +60 -19
  125. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.interface.ts +2 -0
  126. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -5
  127. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +21 -2
  128. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +1 -1
  129. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +1 -1
  130. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +60 -49
  131. package/src/features/billing/stripe-subscription/components/wizards/__tests__/review-card.spec.ts +34 -0
  132. package/src/features/billing/stripe-subscription/components/wizards/review-card.ts +24 -0
  133. package/src/features/billing/stripe-subscription/data/__tests__/stripe-subscription.discount.spec.ts +67 -0
  134. package/src/features/billing/stripe-subscription/data/stripe-subscription.interface.ts +14 -0
  135. package/src/features/billing/stripe-subscription/data/stripe-subscription.service.ts +39 -0
  136. package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +21 -0
  137. package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-payment.spec.ts +48 -0
  138. package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-promotion.spec.ts +132 -0
  139. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +138 -12
  140. package/src/features/billing/stripe-subscription/hooks/wizard-payment.ts +30 -0
  141. package/src/features/billing/stripe-subscription/hooks/wizard-promotion.ts +61 -0
  142. package/src/features/user/contexts/CurrentUserContext.tsx +25 -0
  143. package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +121 -2
  144. package/dist/chunk-G6UE6MEP.js.map +0 -1
  145. package/dist/chunk-TY5FYP3G.js.map +0 -1
  146. package/dist/chunk-VC5A3AUZ.mjs.map +0 -1
  147. package/dist/chunk-X5WIUUNG.mjs.map +0 -1
  148. /package/dist/{BlockNoteEditor-GVQCKRMU.mjs.map → BlockNoteEditor-R3WXUFHX.mjs.map} +0 -0
  149. /package/dist/{BlockNoteViewer-U6E3DS4F.mjs.map → BlockNoteViewer-BVVN6D3T.mjs.map} +0 -0
  150. /package/dist/{chunk-W7F5BDSV.mjs.map → chunk-OKCV4GPJ.mjs.map} +0 -0
  151. /package/dist/{chunk-3CGROPF3.mjs.map → chunk-RGQYFSJF.mjs.map} +0 -0
  152. /package/dist/{chunk-6SQYXUVP.mjs.map → chunk-WCHLCOZG.mjs.map} +0 -0
package/messages/it.json CHANGED
@@ -297,6 +297,118 @@
297
297
  },
298
298
  "billing": {
299
299
  "admin": {
300
+ "coupons": {
301
+ "all_products": "Tutti i prodotti",
302
+ "codes": {
303
+ "automatic": "Automatico",
304
+ "deactivate": {
305
+ "confirm": "Disattiva",
306
+ "description": "Nessuno potrà usare \"{code}\" finché non lo riattivi. Gli abbonamenti che lo hanno già mantengono lo sconto.",
307
+ "title": "Disattiva codice"
308
+ },
309
+ "empty": {
310
+ "description": "Aggiungi un codice che i clienti possono usare per avere lo sconto di questo coupon.",
311
+ "title": "Ancora nessun codice"
312
+ },
313
+ "entity": "codice",
314
+ "never": "Mai",
315
+ "reactivate": {
316
+ "confirm": "Riattiva",
317
+ "description": "\"{code}\" potrà essere usato di nuovo.",
318
+ "title": "Riattiva codice"
319
+ },
320
+ "status": {
321
+ "active": "Attivo",
322
+ "expired": "Scaduto",
323
+ "inactive": "Inattivo",
324
+ "used_up": "Esaurito"
325
+ },
326
+ "title": "Codici"
327
+ },
328
+ "discount": {
329
+ "amount": "{amount} di sconto",
330
+ "forever": "per sempre",
331
+ "once": "una volta",
332
+ "percent": "{value}% di sconto",
333
+ "repeating": "per {months} mesi"
334
+ },
335
+ "duration": {
336
+ "forever": "Per sempre",
337
+ "once": "Una volta",
338
+ "repeating": "Per un numero di mesi"
339
+ },
340
+ "empty": {
341
+ "description": "Crea un coupon per offrire uno sconto al pagamento.",
342
+ "title": "Ancora nessun coupon"
343
+ },
344
+ "entity": "coupon",
345
+ "errors": {
346
+ "amountOff": "Inserisci un importo maggiore di zero",
347
+ "durationInMonths": "Inserisci almeno un mese",
348
+ "minimumAmount": "Inserisci un importo maggiore di zero, o lascia vuoto",
349
+ "name": "Il nome del coupon è obbligatorio",
350
+ "percentOff": "Inserisci una percentuale tra 0,01 e 100"
351
+ },
352
+ "fields": {
353
+ "amountOff": "Importo dello sconto",
354
+ "automatic": "Applica automaticamente",
355
+ "code": "Codice",
356
+ "codeMaxRedemptions": "Limite di utilizzo per questo codice",
357
+ "codes": "Codici",
358
+ "currency": "Valuta",
359
+ "discount": "Sconto",
360
+ "discountType": "Tipo",
361
+ "duration": "Durata",
362
+ "durationInMonths": "Mesi",
363
+ "expiresAt": "Scadenza",
364
+ "firstTimeTransaction": "Solo nuovi clienti",
365
+ "first_time_warning": "Un GM che arriva da una prova conta come cliente esistente, quindi lascia disattivato per i codici pensati per i GM in prova.",
366
+ "maxRedemptions": "Limite di utilizzo totale",
367
+ "minimumAmount": "Importo minimo dell'ordine",
368
+ "name": "Nome",
369
+ "percentOff": "Percentuale di sconto",
370
+ "places": "Posti",
371
+ "products": "Applica a prodotti specifici",
372
+ "redeemBy": "Utilizzabile fino al",
373
+ "status": "Stato",
374
+ "stripeCouponId": "ID",
375
+ "stripeCustomerId": "Limita a un solo cliente",
376
+ "stripeId": "ID coupon Stripe"
377
+ },
378
+ "first_code": "Primo codice",
379
+ "fixed_notice": "Stripe non permette di cambiare sconto, durata o limiti di un coupon. Crea un nuovo coupon.",
380
+ "help": {
381
+ "automatic": "Si applica da solo quando un GM paga la prima volta. Solo un codice può averlo: attivarlo qui lo disattiva sul codice che lo aveva.",
382
+ "products": "Lascia vuoto per applicare il coupon a tutti i prodotti."
383
+ },
384
+ "placeholders": {
385
+ "amountOff": "5,00",
386
+ "code": "Lascia vuoto per farlo generare a Stripe",
387
+ "durationInMonths": "3",
388
+ "maxRedemptions": "Nessun limite",
389
+ "minimumAmount": "0,00",
390
+ "name": "es. GM fondatore",
391
+ "percentOff": "20",
392
+ "stripeCouponId": "Lascia vuoto per farlo generare a Stripe"
393
+ },
394
+ "places": {
395
+ "limited": "{used} / {limit}",
396
+ "no_limit": "Nessun limite",
397
+ "unlimited": "{used} / nessun limite"
398
+ },
399
+ "refresh": {
400
+ "title": "Aggiorna conteggi"
401
+ },
402
+ "status": {
403
+ "active": "Attivo",
404
+ "ended": "Terminato"
405
+ },
406
+ "title": "Coupon",
407
+ "type": {
408
+ "amount_off": "Importo fisso",
409
+ "percent_off": "Percentuale"
410
+ }
411
+ },
300
412
  "prices": {
301
413
  "actions": {
302
414
  "addFeature": "Aggiungi caratteristica",
@@ -430,7 +542,9 @@
430
542
  "archived": "Archiviato"
431
543
  },
432
544
  "tabs": {
433
- "details": "Dettagli"
545
+ "codes": "Codici",
546
+ "details": "Dettagli",
547
+ "products": "Prodotti"
434
548
  },
435
549
  "title": "Prodotti"
436
550
  }
@@ -626,10 +740,12 @@
626
740
  },
627
741
  "promo_code": {
628
742
  "apply": "Applica",
743
+ "automatic_label": "Applicato automaticamente",
629
744
  "duration_forever": "Applicato a tutti i pagamenti",
630
745
  "duration_once": "Applicato solo al primo pagamento",
631
746
  "duration_repeating": "Applicato per {months} mesi",
632
- "placeholder": "Inserisci il codice promozionale"
747
+ "placeholder": "Inserisci il codice promozionale",
748
+ "use_different": "Hai un altro codice?"
633
749
  },
634
750
  "proration": {
635
751
  "net_due_today": "Netto dovuto oggi",
@@ -698,8 +814,10 @@
698
814
  "change_title": "Cambia piano di abbonamento",
699
815
  "errors": {
700
816
  "already_subscribed": "Hai già un abbonamento attivo. Cambia invece il piano attuale.",
817
+ "discount_unavailable": "Questo sconto non è più disponibile. Non ti è stato addebitato nulla.",
701
818
  "invalid_promo_code": "Codice promozionale non valido",
702
819
  "payment_method_required": "Per passare dalla prova a un piano a pagamento serve un metodo di pagamento.",
820
+ "payment_not_confirmed": "Il pagamento non è stato completato. {reason} Non ti è stato addebitato nulla: riprova o aggiungi un'altra carta.",
703
821
  "prepare_review": "Impossibile preparare il riepilogo",
704
822
  "process_subscription": "Impossibile elaborare l'abbonamento",
705
823
  "validate_promo_code": "Impossibile verificare il codice promozionale"
@@ -708,6 +826,7 @@
708
826
  "payment_description": "Inserisci i dati della carta per completare l'abbonamento",
709
827
  "review": {
710
828
  "amount_off": "{amount} di sconto",
829
+ "card_on_file": "{brand} che termina con {last4}",
711
830
  "charge_now": "Importo da addebitare ora:",
712
831
  "confirm_change": "Conferma il cambio di piano",
713
832
  "due_now": "Importo dovuto ora:",
@@ -725,7 +844,10 @@
725
844
  "selected_plan": "Piano selezionato",
726
845
  "subscribe_now": "Abbonati ora",
727
846
  "trial_upgrade": "Passaggio dalla prova",
728
- "trial_upgrade_description": "La tua prova terminerà subito e ti verrà addebitato il prezzo pieno."
847
+ "trial_upgrade_description": "La tua prova terminerà subito e ti verrà addebitato il prezzo pieno.",
848
+ "trial_upgrade_description_discounted": "La tua prova terminerà subito e ti verrà addebitato il prezzo scontato.",
849
+ "use_another_card": "Usa un'altra carta",
850
+ "discount_carries_over": "Il tuo sconto attuale resta valido anche sul nuovo piano."
729
851
  },
730
852
  "steps": {
731
853
  "payment": "Pagamento",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "5.5.0",
3
+ "version": "5.7.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -28,6 +28,7 @@ export const APP_PROVIDED_KEYS = [
28
28
  /^features\.assistant\.starters\./, // example questions about the app's own domain
29
29
  /^features\.assistant\.empty_state\.subtitle$/, // says what the app's assistant knows about
30
30
  /^auth\.gdpr\.marketing_consent$/, // a consent must name who sends the news: the app
31
+ /^billing\.ui\.wizard\.errors\.discount_sold_out$/, // names who to contact: the app
31
32
  // The app's own entities (CommonDeleter / RecentPagesNavigator render
32
33
  // `entities.${type}` for any module). Only the library's own entities ship.
33
34
  /^entities\.(?!(?:assistants|companies|howtos|rbac|roles|users)(?:\.|$))/,
@@ -5,6 +5,7 @@ export * from "../features/billing/stripe-customer/components";
5
5
  export * from "../features/billing/stripe-invoice/components";
6
6
  export * from "../features/billing/stripe-price/components";
7
7
  export * from "../features/billing/stripe-product/components";
8
+ export * from "../features/billing/stripe-coupon/components";
8
9
  export * from "../features/billing/stripe-subscription/components";
9
10
  export * from "../features/billing/stripe-usage/components";
10
11
  export * from "../features/billing/stripe-promotion-code/components";
@@ -21,6 +21,8 @@ import { useRoleTableStructure } from "../features/role/hooks";
21
21
  import { useUserTableStructure } from "../features/user/hooks";
22
22
  import { useStripePriceTableStructure } from "../features/billing/stripe-price/hooks/useStripePriceTableStructure";
23
23
  import { useStripeProductTableStructure } from "../features/billing/stripe-product/hooks/useStripeProductTableStructure";
24
+ import { useStripeCouponTableStructure } from "../features/billing/stripe-coupon/hooks/useStripeCouponTableStructure";
25
+ import { useStripeCouponCodeTableStructure } from "../features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure";
24
26
  import { useWaitlistTableStructure } from "../features/waitlist/hooks/useWaitlistTableStructure";
25
27
  import { registerTableGenerator } from "../hooks";
26
28
 
@@ -35,5 +37,7 @@ registerTableGenerator("users", useUserTableStructure);
35
37
  registerTableGenerator("companies", useCompanyTableStructure);
36
38
  registerTableGenerator("stripe-products", useStripeProductTableStructure);
37
39
  registerTableGenerator("stripe-prices", useStripePriceTableStructure);
40
+ registerTableGenerator("stripe-coupons", useStripeCouponTableStructure);
41
+ registerTableGenerator("stripe-coupon-codes", useStripeCouponCodeTableStructure);
38
42
  registerTableGenerator("waitlists", useWaitlistTableStructure);
39
43
  // Note: Content registration moved to app-level to support app-specific cellTopic
package/src/core/index.ts CHANGED
@@ -56,6 +56,10 @@ export * from "../features/billing/stripe-product";
56
56
  export * from "../features/billing/stripe-subscription";
57
57
  export * from "../features/billing/stripe-usage";
58
58
  export * from "../features/billing/stripe-promotion-code";
59
+ export * from "../features/billing/stripe-coupon";
60
+ // Billing formatters: plain functions, no React, so a server component can format
61
+ // a price. The client-marked ./billing entry exports them too.
62
+ export * from "../features/billing/components/utils/currency";
59
63
  // AI connection administration: data + module only, NOT the feature barrel —
60
64
  // that one also re-exports the feature's client components, and core must stay
61
65
  // importable from the server (same rationale as the tokenusage entries below).
@@ -34,6 +34,9 @@ export interface FoundationModuleDefinitions {
34
34
  StripePrice: ModuleWithPermissions;
35
35
  StripeUsage: ModuleWithPermissions;
36
36
  StripePromotionCode: ModuleWithPermissions;
37
+ StripeCoupon: ModuleWithPermissions;
38
+ StripeCouponCode: ModuleWithPermissions;
39
+ StripeCouponOffer: ModuleWithPermissions;
37
40
  // AI connection administration
38
41
  AiConnection: ModuleWithPermissions;
39
42
  // OAuth modules
@@ -71,8 +71,9 @@ export function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }
71
71
  );
72
72
  }
73
73
 
74
- // Get the most recent invoice
75
- const latestInvoice = invoices[0];
74
+ // The most recent invoice that could charge something: a void one is an unpaid first-payment
75
+ // attempt that was replaced, which never charged the GM (fall back to it only when nothing else exists)
76
+ const latestInvoice = invoices.find((inv) => inv.status !== InvoiceStatus.VOID) ?? invoices[0];
76
77
  const paidInvoices = invoices.filter((inv) => inv.status === InvoiceStatus.PAID);
77
78
  const openInvoices = invoices.filter((inv) => inv.status === InvoiceStatus.OPEN);
78
79
 
@@ -91,7 +91,8 @@ export function SubscriptionSummaryCard({
91
91
  <CreditCard className="h-4 w-4 text-muted-foreground" />
92
92
  </CardHeader>
93
93
  <CardContent>
94
- {subscriptions.length === 0 ? (
94
+ {/* No active or trialing plan (none at all, or only unpaid / ended ones): offer the plans */}
95
+ {!primarySubscription ? (
95
96
  <div className="space-y-2">
96
97
  <p className="text-xl font-bold text-muted-foreground">{t("billing.ui.cards.subscriptions.empty_title")}</p>
97
98
  <p className="text-xs text-muted-foreground">{t("billing.ui.cards.subscriptions.empty_description")}</p>
@@ -108,7 +109,7 @@ export function SubscriptionSummaryCard({
108
109
  <ChevronRight className="h-4 w-4 ms-1 rtl:rotate-180" />
109
110
  </Button>
110
111
  </div>
111
- ) : primarySubscription ? (
112
+ ) : (
112
113
  <div className="space-y-2">
113
114
  <div className="flex items-center gap-2">
114
115
  <p className="text-xl font-bold">
@@ -147,7 +148,7 @@ export function SubscriptionSummaryCard({
147
148
  </p>
148
149
  )}
149
150
  </div>
150
- ) : null}
151
+ )}
151
152
  </CardContent>
152
153
  </Card>
153
154
  );
@@ -22,6 +22,7 @@ import {
22
22
  SubscriptionSummaryCard,
23
23
  } from "../cards";
24
24
  import { BillingDetailModal } from "../modals/BillingDetailModal";
25
+ import { urlWithoutParam } from "../utils/url";
25
26
  import { BillingAlertBanner } from "../widgets/BillingAlertBanner";
26
27
 
27
28
  type ModalType = "subscriptions" | "payment-methods" | "invoices" | "usage" | null;
@@ -282,14 +283,27 @@ export function BillingDashboardContainer() {
282
283
  fetchAllData();
283
284
  }, [fetchAllData]);
284
285
 
285
- // Handle URL action parameter for deep linking
286
+ // Handle URL action parameter for deep linking. `?action=subscribe` records the
287
+ // intent to subscribe; the wizard itself only renders once a customer exists, so a
288
+ // company without one sees the setup card first and the wizard opens right after
289
+ // "Set Up Billing Account" succeeds.
286
290
  useEffect(() => {
287
291
  const action = searchParams.get("action");
288
292
  if (action === "subscribe") {
289
- // Open wizard directly (no wrapper modal)
290
293
  setShowWizard(true);
291
- // Clear the URL param to prevent re-triggering on refresh
292
- window.history.replaceState({}, "", window.location.pathname);
294
+ // Drop only `action`, so a refresh does not re-trigger it. Every other parameter
295
+ // must survive: a host page (the settings hub) selects this dashboard through
296
+ // `?section=`, and losing it switches the page away and unmounts this component.
297
+ window.history.replaceState(
298
+ window.history.state,
299
+ "",
300
+ urlWithoutParam({
301
+ pathname: window.location.pathname,
302
+ search: window.location.search,
303
+ hash: window.location.hash,
304
+ param: "action",
305
+ }),
306
+ );
293
307
  }
294
308
  }, [searchParams]);
295
309
 
@@ -445,9 +459,12 @@ export function BillingDashboardContainer() {
445
459
  onOpenChange={handleModalClose}
446
460
  title={getModalTitle("subscriptions")}
447
461
  >
462
+ {/* The dashboard's list, so a wizard success (refreshData) refreshes the dialog too */}
448
463
  <SubscriptionsContainer
449
464
  onOpenWizard={handleOpenWizard}
450
465
  hasActiveRecurringSubscription={hasActiveRecurringSubscription}
466
+ subscriptions={data.subscriptions}
467
+ onSubscriptionsChange={refreshData}
451
468
  />
452
469
  </BillingDetailModal>
453
470
 
@@ -477,7 +494,7 @@ export function BillingDashboardContainer() {
477
494
 
478
495
  {/* Subscription Wizard - rendered at dashboard level to avoid nested dialogs */}
479
496
  <SubscriptionWizard
480
- open={showWizard}
497
+ open={showWizard && !!data.customer}
481
498
  onOpenChange={(open) => !open && handleWizardClose()}
482
499
  onSuccess={refreshData}
483
500
  hasActiveRecurringSubscription={hasActiveRecurringSubscription}
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { StripeSubscriptionInterface } from "../../../stripe-subscription/data/stripe-subscription.interface";
3
+ import { applyDiscount, promotionCodeDiscount, subscriptionDiscount } from "../discount";
4
+
5
+ const subscription = (discount: StripeSubscriptionInterface["discount"]) =>
6
+ ({ discount }) as unknown as StripeSubscriptionInterface;
7
+
8
+ describe("applyDiscount", () => {
9
+ it("takes a percentage off", () => {
10
+ expect(applyDiscount(999, { discountType: "percent_off", discountValue: 20 })).toBeCloseTo(799.2);
11
+ });
12
+
13
+ it("takes an amount off without going below zero", () => {
14
+ expect(applyDiscount(499, { discountType: "amount_off", discountValue: 100 })).toBe(399);
15
+ expect(applyDiscount(499, { discountType: "amount_off", discountValue: 1000 })).toBe(0);
16
+ });
17
+
18
+ it("returns null without a discount or an amount", () => {
19
+ expect(applyDiscount(999, null)).toBeNull();
20
+ expect(applyDiscount(undefined, { discountType: "percent_off", discountValue: 20 })).toBeNull();
21
+ });
22
+ });
23
+
24
+ describe("promotionCodeDiscount", () => {
25
+ it("reads a valid code", () => {
26
+ expect(
27
+ promotionCodeDiscount({ valid: true, code: "FOUNDER", discountType: "percent_off", discountValue: 20 }),
28
+ ).toEqual({ discountType: "percent_off", discountValue: 20, currency: undefined });
29
+ });
30
+
31
+ it("ignores an invalid or missing code", () => {
32
+ expect(
33
+ promotionCodeDiscount({ valid: false, code: "X", discountType: "percent_off", discountValue: 20 }),
34
+ ).toBeNull();
35
+ expect(promotionCodeDiscount(null)).toBeNull();
36
+ });
37
+ });
38
+
39
+ describe("subscriptionDiscount", () => {
40
+ it("reads a percentage discount", () => {
41
+ expect(subscriptionDiscount(subscription({ percentOff: 20, duration: "forever" }))).toEqual({
42
+ discountType: "percent_off",
43
+ discountValue: 20,
44
+ });
45
+ });
46
+
47
+ it("reads an amount discount with its currency", () => {
48
+ expect(subscriptionDiscount(subscription({ amountOff: 200, currency: "eur", duration: "once" }))).toEqual({
49
+ discountType: "amount_off",
50
+ discountValue: 200,
51
+ currency: "eur",
52
+ });
53
+ });
54
+
55
+ it("returns null when the subscription has no discount", () => {
56
+ expect(subscriptionDiscount(subscription(undefined))).toBeNull();
57
+ expect(subscriptionDiscount(undefined)).toBeNull();
58
+ });
59
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { urlWithoutParam } from "../url";
3
+
4
+ describe("urlWithoutParam", () => {
5
+ it("keeps the section when it drops the action", () => {
6
+ expect(
7
+ urlWithoutParam({ pathname: "/settings", search: "?section=billing&action=subscribe", param: "action" }),
8
+ ).toBe("/settings?section=billing");
9
+ });
10
+
11
+ it("keeps the section when the action comes first", () => {
12
+ expect(
13
+ urlWithoutParam({ pathname: "/it/settings", search: "?action=subscribe&section=billing", param: "action" }),
14
+ ).toBe("/it/settings?section=billing");
15
+ });
16
+
17
+ it("returns the bare path when the action was the only parameter", () => {
18
+ expect(urlWithoutParam({ pathname: "/billing", search: "?action=subscribe", param: "action" })).toBe("/billing");
19
+ });
20
+
21
+ it("leaves a URL without the parameter unchanged, hash included", () => {
22
+ expect(
23
+ urlWithoutParam({ pathname: "/settings", search: "?section=billing", hash: "#plans", param: "action" }),
24
+ ).toBe("/settings?section=billing#plans");
25
+ });
26
+
27
+ it("accepts a search string without the leading question mark", () => {
28
+ expect(
29
+ urlWithoutParam({ pathname: "/settings", search: "section=billing&action=subscribe", param: "action" }),
30
+ ).toBe("/settings?section=billing");
31
+ });
32
+ });
@@ -0,0 +1,38 @@
1
+ import { PromotionCodeValidationResult } from "../../stripe-promotion-code/data/stripe-promotion-code.interface";
2
+ import { StripeSubscriptionInterface } from "../../stripe-subscription/data/stripe-subscription.interface";
3
+
4
+ /** A discount as the billing screens apply it: a percentage, or an amount in minor units. */
5
+ export type BillingDiscount = {
6
+ discountType: "percent_off" | "amount_off";
7
+ discountValue: number;
8
+ currency?: string;
9
+ };
10
+
11
+ /** The discount a valid promotion code (typed or automatic) gives, or null. */
12
+ export function promotionCodeDiscount(code: PromotionCodeValidationResult | null | undefined): BillingDiscount | null {
13
+ if (!code?.valid || !code.discountType || !code.discountValue) return null;
14
+ return { discountType: code.discountType, discountValue: code.discountValue, currency: code.currency };
15
+ }
16
+
17
+ /** The discount a subscription already carries in Stripe, or null. */
18
+ export function subscriptionDiscount(
19
+ subscription: StripeSubscriptionInterface | null | undefined,
20
+ ): BillingDiscount | null {
21
+ const discount = subscription?.discount;
22
+ if (!discount) return null;
23
+ if (discount.percentOff) return { discountType: "percent_off", discountValue: discount.percentOff };
24
+ if (discount.amountOff) {
25
+ return { discountType: "amount_off", discountValue: discount.amountOff, currency: discount.currency };
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * The amount after the discount, in the same minor units; null when there is no
32
+ * discount or nothing to discount. An amount discount never goes below zero.
33
+ */
34
+ export function applyDiscount(amount: number | undefined, discount: BillingDiscount | null): number | null {
35
+ if (!discount || !amount) return null;
36
+ if (discount.discountType === "percent_off") return amount * (1 - discount.discountValue / 100);
37
+ return Math.max(0, amount - discount.discountValue);
38
+ }
@@ -2,3 +2,5 @@ export { formatCurrency, formatFixed, formatInterval } from "./currency";
2
2
  export { formatDate } from "./date";
3
3
  export type { BillingFormatOptions, BillingTranslator } from "./format-options";
4
4
  export { useBillingFormatOptions } from "./useBillingFormatOptions";
5
+ export { applyDiscount, promotionCodeDiscount, subscriptionDiscount } from "./discount";
6
+ export type { BillingDiscount } from "./discount";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The URL to write back after consuming a one-shot query parameter (a deep-link
3
+ * `?action=`). Drops only `param`; every other parameter and the hash stay, so a
4
+ * host page that keys its state on the URL (the settings hub's `?section=`) keeps
5
+ * showing the same place.
6
+ */
7
+ export function urlWithoutParam(params: { pathname: string; search: string; hash?: string; param: string }): string {
8
+ const query = new URLSearchParams(params.search);
9
+ query.delete(params.param);
10
+ const remaining = query.toString();
11
+ return `${params.pathname}${remaining ? `?${remaining}` : ""}${params.hash ?? ""}`;
12
+ }