@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
@@ -0,0 +1,70 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../../core";
2
+ import { StripeCouponOfferInterface } from "./stripe-coupon-offer.interface";
3
+ import { StripeCouponDiscountType, StripeCouponDurationType } from "./stripe-coupon.interface";
4
+
5
+ /** Read-only public resource: never created or updated from the frontend. */
6
+ export class StripeCouponOffer extends AbstractApiData implements StripeCouponOfferInterface {
7
+ private _discountType?: StripeCouponDiscountType;
8
+ private _percentOff?: number;
9
+ private _amountOff?: number;
10
+ private _currency?: string;
11
+ private _duration?: StripeCouponDurationType;
12
+ private _durationInMonths?: number;
13
+ private _placesLeft: number = 0;
14
+ private _placesTotal: number = 0;
15
+
16
+ get discountType(): StripeCouponDiscountType {
17
+ if (!this._discountType) throw new Error("discountType is not defined");
18
+ return this._discountType;
19
+ }
20
+
21
+ get percentOff(): number | undefined {
22
+ return this._percentOff;
23
+ }
24
+
25
+ get amountOff(): number | undefined {
26
+ return this._amountOff;
27
+ }
28
+
29
+ get currency(): string | undefined {
30
+ return this._currency;
31
+ }
32
+
33
+ get duration(): StripeCouponDurationType {
34
+ if (!this._duration) throw new Error("duration is not defined");
35
+ return this._duration;
36
+ }
37
+
38
+ get durationInMonths(): number | undefined {
39
+ return this._durationInMonths;
40
+ }
41
+
42
+ get placesLeft(): number {
43
+ return this._placesLeft;
44
+ }
45
+
46
+ get placesTotal(): number {
47
+ return this._placesTotal;
48
+ }
49
+
50
+ rehydrate(data: JsonApiHydratedDataInterface): this {
51
+ super.rehydrate(data);
52
+
53
+ const attributes = data.jsonApi.attributes;
54
+
55
+ this._discountType = attributes.discountType;
56
+ this._percentOff = attributes.percentOff ?? undefined;
57
+ this._amountOff = attributes.amountOff ?? undefined;
58
+ this._currency = attributes.currency ?? undefined;
59
+ this._duration = attributes.duration;
60
+ this._durationInMonths = attributes.durationInMonths ?? undefined;
61
+ this._placesLeft = attributes.placesLeft ?? 0;
62
+ this._placesTotal = attributes.placesTotal ?? 0;
63
+
64
+ return this;
65
+ }
66
+
67
+ createJsonApi(_data?: any): any {
68
+ throw new Error("StripeCouponOffer is declared by the API and is read-only");
69
+ }
70
+ }
@@ -0,0 +1,7 @@
1
+ export enum StripeCouponFields {
2
+ name = "name",
3
+ discount = "discount",
4
+ places = "places",
5
+ codes = "codes",
6
+ status = "status",
7
+ }
@@ -0,0 +1,55 @@
1
+ import { ApiDataInterface } from "../../../../core";
2
+ import { StripeCouponCodeInput, StripeCouponCodeInterface } from "./stripe-coupon-code.interface";
3
+
4
+ // ============================================================================
5
+ // Stripe Coupon Interfaces
6
+ // ============================================================================
7
+
8
+ export type StripeCouponDiscountType = "percent_off" | "amount_off";
9
+ export type StripeCouponDurationType = "once" | "repeating" | "forever";
10
+
11
+ export interface StripeCouponInterface extends ApiDataInterface {
12
+ get stripeCouponId(): string;
13
+ get name(): string;
14
+ get discountType(): StripeCouponDiscountType;
15
+ get percentOff(): number | undefined;
16
+ get amountOff(): number | undefined; // minor units (cents)
17
+ get currency(): string | undefined;
18
+ get duration(): StripeCouponDurationType;
19
+ get durationInMonths(): number | undefined;
20
+ get stripeProductIds(): string[]; // empty = every product
21
+ get redeemBy(): Date | undefined;
22
+ get maxRedemptions(): number | undefined;
23
+ get timesRedeemed(): number;
24
+ get valid(): boolean;
25
+ get codes(): StripeCouponCodeInterface[];
26
+ }
27
+
28
+ // ============================================================================
29
+ // Stripe Coupon Input DTOs
30
+ // ============================================================================
31
+
32
+ /** The discount of a new coupon: a percentage, or a fixed amount in one currency. */
33
+ export type StripeCouponDiscountInput =
34
+ | { discountType: "percent_off"; percentOff: number }
35
+ | { discountType: "amount_off"; amountOff: number; currency: string }; // amountOff in minor units
36
+
37
+ /** How long a new coupon's discount lasts: months only when it repeats. */
38
+ export type StripeCouponDurationInput =
39
+ { duration: "once" } | { duration: "forever" } | { duration: "repeating"; durationInMonths: number };
40
+
41
+ export type StripeCouponInput = {
42
+ id: string;
43
+ name: string;
44
+ stripeCouponId?: string;
45
+ productIds?: string[]; // our StripeProduct ids
46
+ redeemBy?: Date;
47
+ maxRedemptions?: number;
48
+ firstCode: Omit<StripeCouponCodeInput, "id" | "couponId">;
49
+ } & StripeCouponDiscountInput &
50
+ StripeCouponDurationInput;
51
+
52
+ export type StripeCouponRenameInput = {
53
+ id: string;
54
+ name: string;
55
+ };
@@ -0,0 +1,93 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef, PreviousRef } from "../../../../core";
2
+ import { STRIPE_COUPON_REFRESH_SEGMENT } from "../stripe-coupon.module";
3
+ import { StripeCoupon } from "./stripe-coupon";
4
+ import { StripeCouponInput, StripeCouponInterface, StripeCouponRenameInput } from "./stripe-coupon.interface";
5
+
6
+ /**
7
+ * Admin billing service for managing coupons
8
+ */
9
+ export class StripeCouponService extends AbstractService {
10
+ /**
11
+ * List all coupons with their codes (admin)
12
+ */
13
+ static async listCoupons(params?: { next?: NextRef; prev?: PreviousRef }): Promise<StripeCouponInterface[]> {
14
+ const endpoint = new EndpointCreator({
15
+ endpoint: Modules.StripeCoupon,
16
+ });
17
+
18
+ return this.callApi({
19
+ type: Modules.StripeCoupon,
20
+ method: HttpMethod.GET,
21
+ endpoint: endpoint.generate(),
22
+ next: params?.next,
23
+ previous: params?.prev,
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Get a specific coupon by ID (admin)
29
+ */
30
+ static async getCoupon(params: { id: string }): Promise<StripeCouponInterface> {
31
+ const endpoint = new EndpointCreator({
32
+ endpoint: Modules.StripeCoupon,
33
+ id: params.id,
34
+ });
35
+
36
+ return this.callApi<StripeCouponInterface>({
37
+ type: Modules.StripeCoupon,
38
+ method: HttpMethod.GET,
39
+ endpoint: endpoint.generate(),
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Create a coupon and its first code (admin)
45
+ */
46
+ static async createCoupon(params: StripeCouponInput): Promise<StripeCouponInterface> {
47
+ const endpoint = new EndpointCreator({
48
+ endpoint: Modules.StripeCoupon,
49
+ });
50
+
51
+ return this.callApi<StripeCouponInterface>({
52
+ type: Modules.StripeCoupon,
53
+ method: HttpMethod.POST,
54
+ endpoint: endpoint.generate(),
55
+ input: params,
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Rename a coupon (admin). The only coupon field Stripe allows to change.
61
+ */
62
+ static async renameCoupon(params: StripeCouponRenameInput): Promise<StripeCouponInterface> {
63
+ const endpoint = new EndpointCreator({
64
+ endpoint: Modules.StripeCoupon,
65
+ id: params.id,
66
+ });
67
+
68
+ return this.callApi<StripeCouponInterface>({
69
+ type: Modules.StripeCoupon,
70
+ method: HttpMethod.PATCH,
71
+ endpoint: endpoint.generate(),
72
+ input: new StripeCoupon().createRenameJsonApi(params),
73
+ overridesJsonApiCreation: true,
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Re-read the coupon and its codes from Stripe and store the counts (admin)
79
+ */
80
+ static async refreshCounts(params: { id: string }): Promise<StripeCouponInterface> {
81
+ const endpoint = new EndpointCreator({
82
+ endpoint: Modules.StripeCoupon,
83
+ id: params.id,
84
+ childEndpoint: STRIPE_COUPON_REFRESH_SEGMENT,
85
+ });
86
+
87
+ return this.callApi<StripeCouponInterface>({
88
+ type: Modules.StripeCoupon,
89
+ method: HttpMethod.POST,
90
+ endpoint: endpoint.generate(),
91
+ });
92
+ }
93
+ }
@@ -0,0 +1,167 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../../core";
2
+ import { StripeCouponCodeInput, StripeCouponCodeInterface } from "./stripe-coupon-code.interface";
3
+ import {
4
+ StripeCouponDiscountType,
5
+ StripeCouponDurationType,
6
+ StripeCouponInput,
7
+ StripeCouponInterface,
8
+ StripeCouponRenameInput,
9
+ } from "./stripe-coupon.interface";
10
+
11
+ export class StripeCoupon extends AbstractApiData implements StripeCouponInterface {
12
+ private _stripeCouponId?: string;
13
+ private _name?: string;
14
+ private _discountType?: StripeCouponDiscountType;
15
+ private _percentOff?: number;
16
+ private _amountOff?: number;
17
+ private _currency?: string;
18
+ private _duration?: StripeCouponDurationType;
19
+ private _durationInMonths?: number;
20
+ private _stripeProductIds: string[] = [];
21
+ private _redeemBy?: Date;
22
+ private _maxRedemptions?: number;
23
+ private _timesRedeemed: number = 0;
24
+ private _valid: boolean = true;
25
+ private _codes: StripeCouponCodeInterface[] = [];
26
+
27
+ get stripeCouponId(): string {
28
+ if (!this._stripeCouponId) throw new Error("stripeCouponId is not defined");
29
+ return this._stripeCouponId;
30
+ }
31
+
32
+ get name(): string {
33
+ return this._name ?? "";
34
+ }
35
+
36
+ get discountType(): StripeCouponDiscountType {
37
+ if (!this._discountType) throw new Error("discountType is not defined");
38
+ return this._discountType;
39
+ }
40
+
41
+ get percentOff(): number | undefined {
42
+ return this._percentOff;
43
+ }
44
+
45
+ get amountOff(): number | undefined {
46
+ return this._amountOff;
47
+ }
48
+
49
+ get currency(): string | undefined {
50
+ return this._currency;
51
+ }
52
+
53
+ get duration(): StripeCouponDurationType {
54
+ if (!this._duration) throw new Error("duration is not defined");
55
+ return this._duration;
56
+ }
57
+
58
+ get durationInMonths(): number | undefined {
59
+ return this._durationInMonths;
60
+ }
61
+
62
+ get stripeProductIds(): string[] {
63
+ return this._stripeProductIds;
64
+ }
65
+
66
+ get redeemBy(): Date | undefined {
67
+ return this._redeemBy;
68
+ }
69
+
70
+ get maxRedemptions(): number | undefined {
71
+ return this._maxRedemptions;
72
+ }
73
+
74
+ get timesRedeemed(): number {
75
+ return this._timesRedeemed;
76
+ }
77
+
78
+ get valid(): boolean {
79
+ return this._valid;
80
+ }
81
+
82
+ get codes(): StripeCouponCodeInterface[] {
83
+ return this._codes;
84
+ }
85
+
86
+ rehydrate(data: JsonApiHydratedDataInterface): this {
87
+ super.rehydrate(data);
88
+
89
+ const attributes = data.jsonApi.attributes;
90
+
91
+ this._stripeCouponId = attributes.stripeCouponId;
92
+ this._name = attributes.name;
93
+ this._discountType = attributes.discountType;
94
+ this._percentOff = attributes.percentOff ?? undefined;
95
+ this._amountOff = attributes.amountOff ?? undefined;
96
+ this._currency = attributes.currency ?? undefined;
97
+ this._duration = attributes.duration;
98
+ this._durationInMonths = attributes.durationInMonths ?? undefined;
99
+ this._stripeProductIds = attributes.stripeProductIds ?? [];
100
+ this._redeemBy = attributes.redeemBy ? new Date(attributes.redeemBy) : undefined;
101
+ this._maxRedemptions = attributes.maxRedemptions ?? undefined;
102
+ this._timesRedeemed = attributes.timesRedeemed ?? 0;
103
+ this._valid = attributes.valid ?? true;
104
+
105
+ this._codes =
106
+ (this._readIncluded(data, "codes", Modules.StripeCouponCode) as StripeCouponCodeInterface[] | undefined) ?? [];
107
+
108
+ return this;
109
+ }
110
+
111
+ createJsonApi(data: StripeCouponInput): any {
112
+ const response: any = {
113
+ data: {
114
+ type: Modules.StripeCoupon.name,
115
+ id: data.id,
116
+ attributes: {
117
+ name: data.name,
118
+ discountType: data.discountType,
119
+ duration: data.duration,
120
+ firstCode: StripeCoupon.codeAttributes(data.firstCode),
121
+ },
122
+ },
123
+ };
124
+
125
+ const attributes = response.data.attributes;
126
+ if (data.stripeCouponId) attributes.stripeCouponId = data.stripeCouponId;
127
+ if (data.discountType === "percent_off") attributes.percentOff = data.percentOff;
128
+ else {
129
+ attributes.amountOff = data.amountOff;
130
+ attributes.currency = data.currency;
131
+ }
132
+ if (data.duration === "repeating") attributes.durationInMonths = data.durationInMonths;
133
+ if (data.productIds && data.productIds.length > 0) attributes.productIds = data.productIds;
134
+ if (data.redeemBy !== undefined) attributes.redeemBy = data.redeemBy.toISOString();
135
+ if (data.maxRedemptions !== undefined) attributes.maxRedemptions = data.maxRedemptions;
136
+
137
+ return response;
138
+ }
139
+
140
+ /**
141
+ * Rename is the only change Stripe allows on a coupon after creation.
142
+ * Dedicated method so the service never needs a generic override.
143
+ */
144
+ createRenameJsonApi(data: StripeCouponRenameInput): any {
145
+ return {
146
+ data: {
147
+ type: Modules.StripeCoupon.name,
148
+ id: data.id,
149
+ attributes: { name: data.name },
150
+ },
151
+ };
152
+ }
153
+
154
+ /** Attributes of a code, shared by the coupon's first code and StripeCouponCode.createJsonApi. */
155
+ static codeAttributes(code: Omit<StripeCouponCodeInput, "id" | "couponId">): Record<string, any> {
156
+ const attributes: Record<string, any> = {};
157
+ if (code.code) attributes.code = code.code;
158
+ if (code.firstTimeTransaction !== undefined) attributes.firstTimeTransaction = code.firstTimeTransaction;
159
+ if (code.minimumAmount !== undefined) attributes.minimumAmount = code.minimumAmount;
160
+ if (code.minimumAmountCurrency) attributes.minimumAmountCurrency = code.minimumAmountCurrency;
161
+ if (code.stripeCustomerId) attributes.stripeCustomerId = code.stripeCustomerId;
162
+ if (code.expiresAt !== undefined) attributes.expiresAt = code.expiresAt.toISOString();
163
+ if (code.maxRedemptions !== undefined) attributes.maxRedemptions = code.maxRedemptions;
164
+ if (code.automatic !== undefined) attributes.automatic = code.automatic;
165
+ return attributes;
166
+ }
167
+ }
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+ import { ColumnDef } from "@tanstack/react-table";
4
+ import { useTranslations } from "next-intl";
5
+ import { useMemo, useState } from "react";
6
+ import { Badge } from "../../../../shadcnui";
7
+ import { registerTableGenerator, TableContent, UseTableStructureHook } from "../../../../hooks";
8
+ import { formatDate, useBillingFormatOptions } from "../../components/utils";
9
+ import { useCouponContext } from "../../contexts/CouponContext";
10
+ import { CouponCodeActions } from "../components/widgets/CouponCodeActions";
11
+ import { StripeCouponCodeFields } from "../data/stripe-coupon-code.fields";
12
+ import { StripeCouponCodeInterface } from "../data/stripe-coupon-code.interface";
13
+ import { StripeCouponInterface } from "../data/stripe-coupon.interface";
14
+ import { couponPlacesLeft } from "../utils/coupon-places";
15
+
16
+ export type CouponCodeStatus = "active" | "inactive" | "expired" | "used_up";
17
+
18
+ /**
19
+ * Expired and used-up codes are dead for good (Stripe never revives them), so
20
+ * they win over a plain inactive flag. Places follow the same rule as the
21
+ * backend: the smaller of the code's and the coupon's remaining limits.
22
+ */
23
+ export function couponCodeStatus(
24
+ code: StripeCouponCodeInterface,
25
+ coupon: StripeCouponInterface | undefined,
26
+ now: Date,
27
+ ): CouponCodeStatus {
28
+ if (code.expiresAt && code.expiresAt.getTime() <= now.getTime()) return "expired";
29
+ const left = couponPlacesLeft(code, coupon ?? { maxRedemptions: undefined, timesRedeemed: 0 });
30
+ if (left === 0) return "used_up";
31
+ return code.active ? "active" : "inactive";
32
+ }
33
+
34
+ const STATUS_VARIANT: Record<CouponCodeStatus, "softGreen" | "softGray" | "softAmber" | "softRed"> = {
35
+ active: "softGreen",
36
+ inactive: "softGray",
37
+ expired: "softAmber",
38
+ used_up: "softRed",
39
+ };
40
+
41
+ export const useStripeCouponCodeTableStructure: UseTableStructureHook<
42
+ StripeCouponCodeInterface,
43
+ StripeCouponCodeFields
44
+ > = (params) => {
45
+ const t = useTranslations();
46
+ const fmt = useBillingFormatOptions();
47
+ // The codes list is mounted on the coupon's own page, so the coupon whose
48
+ // limits share the places is the context's, not whatever the code includes.
49
+ const { coupon } = useCouponContext();
50
+ // Read once on mount: render stays pure, and "expired" is judged against the
51
+ // moment the list was opened.
52
+ const [now] = useState(() => new Date());
53
+
54
+ const tableData = useMemo(() => {
55
+ return params.data.map((code: StripeCouponCodeInterface) => {
56
+ const entry: TableContent<StripeCouponCodeInterface> = { jsonApiData: code };
57
+ params.fields.forEach((field) => {
58
+ entry[field] = (code as any)[field as keyof StripeCouponCodeInterface];
59
+ });
60
+ return entry;
61
+ });
62
+ }, [params.data, params.fields]);
63
+
64
+ const fieldColumnMap: Partial<Record<StripeCouponCodeFields, () => any>> = {
65
+ [StripeCouponCodeFields.code]: () => ({
66
+ id: "code",
67
+ accessorKey: "code",
68
+ header: t("billing.admin.coupons.fields.code"),
69
+ cell: ({ row }: { row: TableContent<StripeCouponCodeInterface> }) => {
70
+ const code: StripeCouponCodeInterface = row.original.jsonApiData;
71
+ return <span className="text-xs">{code.code}</span>;
72
+ },
73
+ enableSorting: false,
74
+ enableHiding: false,
75
+ }),
76
+ [StripeCouponCodeFields.places]: () => ({
77
+ id: "places",
78
+ accessorKey: "places",
79
+ header: t("billing.admin.coupons.fields.places"),
80
+ cell: ({ row }: { row: TableContent<StripeCouponCodeInterface> }) => {
81
+ const code: StripeCouponCodeInterface = row.original.jsonApiData;
82
+ // Typography role 15 (numeric): tabular-nums, end-aligned.
83
+ return (
84
+ <span className="block text-xs tabular-nums text-end">
85
+ {code.maxRedemptions !== undefined
86
+ ? t("billing.admin.coupons.places.limited", {
87
+ used: String(code.timesRedeemed),
88
+ limit: String(code.maxRedemptions),
89
+ })
90
+ : t("billing.admin.coupons.places.unlimited", { used: String(code.timesRedeemed) })}
91
+ </span>
92
+ );
93
+ },
94
+ enableSorting: false,
95
+ enableHiding: false,
96
+ }),
97
+ [StripeCouponCodeFields.expiresAt]: () => ({
98
+ id: "expiresAt",
99
+ accessorKey: "expiresAt",
100
+ header: t("billing.admin.coupons.fields.expiresAt"),
101
+ cell: ({ row }: { row: TableContent<StripeCouponCodeInterface> }) => {
102
+ const code: StripeCouponCodeInterface = row.original.jsonApiData;
103
+ return (
104
+ <span className="text-xs tabular-nums">
105
+ {code.expiresAt ? formatDate(code.expiresAt, fmt) : t("billing.admin.coupons.codes.never")}
106
+ </span>
107
+ );
108
+ },
109
+ enableSorting: false,
110
+ enableHiding: false,
111
+ }),
112
+ [StripeCouponCodeFields.automatic]: () => ({
113
+ id: "automatic",
114
+ accessorKey: "automatic",
115
+ header: t("billing.admin.coupons.fields.automatic"),
116
+ cell: ({ row }: { row: TableContent<StripeCouponCodeInterface> }) => {
117
+ const code: StripeCouponCodeInterface = row.original.jsonApiData;
118
+ return code.automatic ? (
119
+ <Badge variant="softBlue">{t("billing.admin.coupons.codes.automatic")}</Badge>
120
+ ) : (
121
+ <span className="text-muted-foreground text-xs">—</span>
122
+ );
123
+ },
124
+ enableSorting: false,
125
+ enableHiding: false,
126
+ }),
127
+ [StripeCouponCodeFields.status]: () => ({
128
+ id: "status",
129
+ accessorKey: "status",
130
+ header: t("billing.admin.coupons.fields.status"),
131
+ cell: ({ row }: { row: TableContent<StripeCouponCodeInterface> }) => {
132
+ const code: StripeCouponCodeInterface = row.original.jsonApiData;
133
+ const status = couponCodeStatus(code, code.coupon ?? coupon, now);
134
+ return <Badge variant={STATUS_VARIANT[status]}>{t(`billing.admin.coupons.codes.status.${status}`)}</Badge>;
135
+ },
136
+ enableSorting: false,
137
+ enableHiding: false,
138
+ }),
139
+ [StripeCouponCodeFields.actions]: () => ({
140
+ id: "actions",
141
+ accessorKey: "actions",
142
+ header: "",
143
+ cell: ({ row }: { row: TableContent<StripeCouponCodeInterface> }) => {
144
+ const code: StripeCouponCodeInterface = row.original.jsonApiData;
145
+ return <CouponCodeActions code={code} status={couponCodeStatus(code, code.coupon ?? coupon, now)} />;
146
+ },
147
+ enableSorting: false,
148
+ enableHiding: false,
149
+ }),
150
+ };
151
+
152
+ const columns = useMemo(() => {
153
+ return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
154
+ TableContent<StripeCouponCodeInterface>
155
+ >[];
156
+ }, [params.fields, fieldColumnMap, t, coupon, now]);
157
+
158
+ return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
159
+ };
160
+
161
+ registerTableGenerator("stripe-coupon-codes", useStripeCouponCodeTableStructure);
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import { ColumnDef } from "@tanstack/react-table";
4
+ import { useTranslations } from "next-intl";
5
+ import { useMemo } from "react";
6
+ import { Badge, Link } from "../../../../shadcnui";
7
+ import { registerTableGenerator, TableContent, usePageUrlGenerator, UseTableStructureHook } from "../../../../hooks";
8
+ import { useBillingFormatOptions } from "../../components/utils";
9
+ import { StripeCouponFields } from "../data/stripe-coupon.fields";
10
+ import { STRIPE_COUPON_DETAIL_PAGE_URL } from "../stripe-coupon.module";
11
+ import { StripeCouponInterface } from "../data/stripe-coupon.interface";
12
+ import { formatCouponDiscount } from "../utils/format-coupon-discount";
13
+
14
+ export const useStripeCouponTableStructure: UseTableStructureHook<StripeCouponInterface, StripeCouponFields> = (
15
+ params,
16
+ ) => {
17
+ const t = useTranslations();
18
+ const fmt = useBillingFormatOptions();
19
+ const generateUrl = usePageUrlGenerator();
20
+
21
+ const tableData = useMemo(() => {
22
+ return params.data.map((coupon: StripeCouponInterface) => {
23
+ const entry: TableContent<StripeCouponInterface> = { jsonApiData: coupon };
24
+ params.fields.forEach((field) => {
25
+ entry[field] = (coupon as any)[field as keyof StripeCouponInterface];
26
+ });
27
+ return entry;
28
+ });
29
+ }, [params.data, params.fields]);
30
+
31
+ const fieldColumnMap: Partial<Record<StripeCouponFields, () => any>> = {
32
+ [StripeCouponFields.name]: () => ({
33
+ id: "name",
34
+ accessorKey: "name",
35
+ header: t("billing.admin.coupons.fields.name"),
36
+ cell: ({ row }: { row: TableContent<StripeCouponInterface> }) => {
37
+ const coupon: StripeCouponInterface = row.original.jsonApiData;
38
+ return <Link href={generateUrl({ page: STRIPE_COUPON_DETAIL_PAGE_URL, id: coupon.id })}>{coupon.name}</Link>;
39
+ },
40
+ enableSorting: false,
41
+ enableHiding: false,
42
+ }),
43
+ [StripeCouponFields.discount]: () => ({
44
+ id: "discount",
45
+ accessorKey: "discount",
46
+ header: t("billing.admin.coupons.fields.discount"),
47
+ cell: ({ row }: { row: TableContent<StripeCouponInterface> }) => {
48
+ const coupon: StripeCouponInterface = row.original.jsonApiData;
49
+ return <span className="text-xs">{formatCouponDiscount(coupon, t, fmt)}</span>;
50
+ },
51
+ enableSorting: false,
52
+ enableHiding: false,
53
+ }),
54
+ [StripeCouponFields.places]: () => ({
55
+ id: "places",
56
+ accessorKey: "places",
57
+ header: t("billing.admin.coupons.fields.places"),
58
+ cell: ({ row }: { row: TableContent<StripeCouponInterface> }) => {
59
+ const coupon: StripeCouponInterface = row.original.jsonApiData;
60
+ // Typography role 15 (numeric): tabular-nums, end-aligned. NEVER
61
+ // font-mono — no monospace font is loaded in these apps.
62
+ return (
63
+ <span className="block text-xs tabular-nums text-end">
64
+ {coupon.maxRedemptions !== undefined
65
+ ? t("billing.admin.coupons.places.limited", {
66
+ used: String(coupon.timesRedeemed),
67
+ limit: String(coupon.maxRedemptions),
68
+ })
69
+ : t("billing.admin.coupons.places.unlimited", { used: String(coupon.timesRedeemed) })}
70
+ </span>
71
+ );
72
+ },
73
+ enableSorting: false,
74
+ enableHiding: false,
75
+ }),
76
+ [StripeCouponFields.codes]: () => ({
77
+ id: "codes",
78
+ accessorKey: "codes",
79
+ header: t("billing.admin.coupons.fields.codes"),
80
+ cell: ({ row }: { row: TableContent<StripeCouponInterface> }) => {
81
+ const coupon: StripeCouponInterface = row.original.jsonApiData;
82
+ return <span className="block text-xs tabular-nums text-end">{coupon.codes.length}</span>;
83
+ },
84
+ enableSorting: false,
85
+ enableHiding: false,
86
+ }),
87
+ [StripeCouponFields.status]: () => ({
88
+ id: "status",
89
+ accessorKey: "status",
90
+ header: t("billing.admin.coupons.fields.status"),
91
+ cell: ({ row }: { row: TableContent<StripeCouponInterface> }) => {
92
+ const coupon: StripeCouponInterface = row.original.jsonApiData;
93
+ return coupon.valid ? (
94
+ <Badge variant="softGreen">{t("billing.admin.coupons.status.active")}</Badge>
95
+ ) : (
96
+ <Badge variant="softGray">{t("billing.admin.coupons.status.ended")}</Badge>
97
+ );
98
+ },
99
+ enableSorting: false,
100
+ enableHiding: false,
101
+ }),
102
+ };
103
+
104
+ const columns = useMemo(() => {
105
+ return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
106
+ TableContent<StripeCouponInterface>
107
+ >[];
108
+ }, [params.fields, fieldColumnMap, t, generateUrl]);
109
+
110
+ return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
111
+ };
112
+
113
+ registerTableGenerator("stripe-coupons", useStripeCouponTableStructure);
@@ -0,0 +1,5 @@
1
+ export * from "./data";
2
+ export * from "./utils";
3
+ export * from "./stripe-coupon.module";
4
+ export * from "./stripe-coupon-code.module";
5
+ export * from "./stripe-coupon-offer.module";