@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.
- package/dist/{BlockNoteEditor-GVQCKRMU.mjs → BlockNoteEditor-R3WXUFHX.mjs} +4 -4
- package/dist/{BlockNoteEditor-THBJH2UR.js → BlockNoteEditor-ZHXXFWUJ.js} +19 -19
- package/dist/{BlockNoteEditor-THBJH2UR.js.map → BlockNoteEditor-ZHXXFWUJ.js.map} +1 -1
- package/dist/{BlockNoteViewer-U6E3DS4F.mjs → BlockNoteViewer-BVVN6D3T.mjs} +2 -2
- package/dist/{BlockNoteViewer-VZOVKTFO.js → BlockNoteViewer-SD3ZFIXD.js} +3 -3
- package/dist/{BlockNoteViewer-VZOVKTFO.js.map → BlockNoteViewer-SD3ZFIXD.js.map} +1 -1
- package/dist/billing/index.d.mts +197 -46
- package/dist/billing/index.d.ts +197 -46
- package/dist/billing/index.js +1166 -589
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +803 -226
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-WPYT4UKJ.js → chunk-2TXE57LE.js} +3 -3
- package/dist/{chunk-WPYT4UKJ.js.map → chunk-2TXE57LE.js.map} +1 -1
- package/dist/{chunk-G6UE6MEP.js → chunk-5TH2DSAH.js} +901 -204
- package/dist/chunk-5TH2DSAH.js.map +1 -0
- package/dist/{chunk-X5WIUUNG.mjs → chunk-CFB6FHIY.mjs} +3342 -2377
- package/dist/chunk-CFB6FHIY.mjs.map +1 -0
- package/dist/{chunk-VC5A3AUZ.mjs → chunk-FK26NX3S.mjs} +701 -4
- package/dist/chunk-FK26NX3S.mjs.map +1 -0
- package/dist/{chunk-YDYVJM4I.js → chunk-IJME5SAM.js} +4 -4
- package/dist/{chunk-YDYVJM4I.js.map → chunk-IJME5SAM.js.map} +1 -1
- package/dist/{chunk-W7F5BDSV.mjs → chunk-OKCV4GPJ.mjs} +2 -2
- package/dist/{chunk-3CGROPF3.mjs → chunk-RGQYFSJF.mjs} +2 -2
- package/dist/{chunk-6SQYXUVP.mjs → chunk-WCHLCOZG.mjs} +2 -2
- package/dist/{chunk-S4YJ5WCW.js → chunk-XOSC3FFP.js} +8 -8
- package/dist/{chunk-S4YJ5WCW.js.map → chunk-XOSC3FFP.js.map} +1 -1
- package/dist/{chunk-TY5FYP3G.js → chunk-ZCOPOO7T.js} +2024 -1059
- package/dist/chunk-ZCOPOO7T.js.map +1 -0
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +248 -3
- package/dist/core/index.d.ts +248 -3
- package/dist/core/index.js +42 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +41 -1
- package/dist/{stripe-promotion-code.interface-ClZ7DxS9.d.mts → currency-DT_dveBx.d.mts} +137 -1
- package/dist/{stripe-promotion-code.interface-C3qqh3mi.d.ts → currency-pK9eRYRC.d.ts} +137 -1
- package/dist/email-automations/index.js +92 -92
- package/dist/email-automations/index.mjs +3 -3
- package/dist/features/analytics/index.js +121 -121
- package/dist/features/analytics/index.mjs +4 -4
- package/dist/features/analytics/server.js +4 -4
- package/dist/features/analytics/server.mjs +2 -2
- package/dist/features/blog/index.js +150 -150
- package/dist/features/blog/index.mjs +3 -3
- package/dist/features/blog/server.js +16 -16
- package/dist/features/blog/server.mjs +1 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +124 -124
- package/dist/features/tokenusage/index.mjs +4 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +43 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/messages/en.json +125 -3
- package/messages/it.json +125 -3
- package/package.json +1 -1
- package/scripts/validate-messages.mjs +1 -0
- package/src/billing/index.ts +1 -0
- package/src/client/index.ts +4 -0
- package/src/core/index.ts +4 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +3 -2
- package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +4 -3
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +22 -5
- package/src/features/billing/components/utils/__tests__/discount.spec.ts +59 -0
- package/src/features/billing/components/utils/__tests__/url.spec.ts +32 -0
- package/src/features/billing/components/utils/discount.ts +38 -0
- package/src/features/billing/components/utils/index.ts +2 -0
- package/src/features/billing/components/utils/url.ts +12 -0
- package/src/features/billing/contexts/CouponContext.tsx +209 -0
- package/src/features/billing/contexts/index.ts +1 -0
- package/src/features/billing/i18n-keys.ts +79 -0
- package/src/features/billing/stripe-coupon/components/containers/CouponContainer.tsx +38 -0
- package/src/features/billing/stripe-coupon/components/containers/index.ts +1 -0
- package/src/features/billing/stripe-coupon/components/details/CouponDetails.tsx +72 -0
- package/src/features/billing/stripe-coupon/components/details/index.ts +1 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx +80 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeFields.tsx +214 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponEditor.tsx +425 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponRefresher.tsx +45 -0
- package/src/features/billing/stripe-coupon/components/forms/index.ts +6 -0
- package/src/features/billing/stripe-coupon/components/index.ts +5 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +61 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +67 -0
- package/src/features/billing/stripe-coupon/components/lists/index.ts +2 -0
- package/src/features/billing/stripe-coupon/components/widgets/CouponCodeActions.tsx +81 -0
- package/src/features/billing/stripe-coupon/components/widgets/index.ts +1 -0
- package/src/features/billing/stripe-coupon/data/__tests__/stripe-coupon-models.spec.ts +114 -0
- package/src/features/billing/stripe-coupon/data/index.ts +10 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.fields.ts +8 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.interface.ts +44 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +92 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.ts +132 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.interface.ts +17 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.ts +70 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.fields.ts +7 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.interface.ts +55 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +93 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.ts +167 -0
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +161 -0
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +113 -0
- package/src/features/billing/stripe-coupon/index.ts +5 -0
- package/src/features/billing/stripe-coupon/stripe-coupon-code.module.ts +15 -0
- package/src/features/billing/stripe-coupon/stripe-coupon-offer.module.ts +10 -0
- package/src/features/billing/stripe-coupon/stripe-coupon.module.ts +25 -0
- package/src/features/billing/stripe-coupon/utils/__tests__/coupon-places.spec.ts +19 -0
- package/src/features/billing/stripe-coupon/utils/__tests__/to-promotion-code-result.spec.ts +41 -0
- package/src/features/billing/stripe-coupon/utils/coupon-places.ts +23 -0
- package/src/features/billing/stripe-coupon/utils/format-coupon-discount.ts +36 -0
- package/src/features/billing/stripe-coupon/utils/index.ts +3 -0
- package/src/features/billing/stripe-coupon/utils/to-promotion-code-result.ts +23 -0
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +24 -8
- package/src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx +21 -6
- package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +60 -19
- package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.interface.ts +2 -0
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -5
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +21 -2
- package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +1 -1
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +1 -1
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +60 -49
- package/src/features/billing/stripe-subscription/components/wizards/__tests__/review-card.spec.ts +34 -0
- package/src/features/billing/stripe-subscription/components/wizards/review-card.ts +24 -0
- package/src/features/billing/stripe-subscription/data/__tests__/stripe-subscription.discount.spec.ts +67 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.interface.ts +14 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.service.ts +39 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +21 -0
- package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-payment.spec.ts +48 -0
- package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-promotion.spec.ts +132 -0
- package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +138 -12
- package/src/features/billing/stripe-subscription/hooks/wizard-payment.ts +30 -0
- package/src/features/billing/stripe-subscription/hooks/wizard-promotion.ts +61 -0
- package/src/features/user/contexts/CurrentUserContext.tsx +25 -0
- package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +121 -2
- package/dist/chunk-G6UE6MEP.js.map +0 -1
- package/dist/chunk-TY5FYP3G.js.map +0 -1
- package/dist/chunk-VC5A3AUZ.mjs.map +0 -1
- package/dist/chunk-X5WIUUNG.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-GVQCKRMU.mjs.map → BlockNoteEditor-R3WXUFHX.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-U6E3DS4F.mjs.map → BlockNoteViewer-BVVN6D3T.mjs.map} +0 -0
- /package/dist/{chunk-W7F5BDSV.mjs.map → chunk-OKCV4GPJ.mjs.map} +0 -0
- /package/dist/{chunk-3CGROPF3.mjs.map → chunk-RGQYFSJF.mjs.map} +0 -0
- /package/dist/{chunk-6SQYXUVP.mjs.map → chunk-WCHLCOZG.mjs.map} +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { TicketIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ReactNode, useEffect } from "react";
|
|
6
|
+
import { ContentListTable } from "../../../../../components";
|
|
7
|
+
import { Modules } from "../../../../../core";
|
|
8
|
+
import { DataListRetriever, useDataListRetriever } from "../../../../../hooks";
|
|
9
|
+
import { EmptyState } from "../../../../../shadcnui";
|
|
10
|
+
import { useCouponContext } from "../../../contexts/CouponContext";
|
|
11
|
+
import { StripeCouponCodeFields } from "../../data/stripe-coupon-code.fields";
|
|
12
|
+
import { StripeCouponCodeInterface } from "../../data/stripe-coupon-code.interface";
|
|
13
|
+
import { StripeCouponCodeService } from "../../data/stripe-coupon-code.service";
|
|
14
|
+
import CouponCodeEditor from "../forms/CouponCodeEditor";
|
|
15
|
+
|
|
16
|
+
type CouponCodesListProps = {
|
|
17
|
+
couponId: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function CouponCodesList({ couponId }: CouponCodesListProps) {
|
|
21
|
+
const t = useTranslations();
|
|
22
|
+
const { catalogueVersion } = useCouponContext();
|
|
23
|
+
|
|
24
|
+
const data: DataListRetriever<StripeCouponCodeInterface> = useDataListRetriever({
|
|
25
|
+
retriever: (params) => StripeCouponCodeService.listByCoupon({ ...params, couponId }),
|
|
26
|
+
retrieverParams: {},
|
|
27
|
+
module: Modules.StripeCouponCode,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Switching "Apply automatically" on one code clears it on the others in the
|
|
31
|
+
// same server write, so every flag change re-reads the whole list.
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (catalogueVersion > 0) void data.refresh();
|
|
34
|
+
}, [catalogueVersion]);
|
|
35
|
+
|
|
36
|
+
const functions: ReactNode[] = [<CouponCodeEditor key="create-code" couponId={couponId} />];
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ContentListTable
|
|
40
|
+
data={data}
|
|
41
|
+
fields={[
|
|
42
|
+
StripeCouponCodeFields.code,
|
|
43
|
+
StripeCouponCodeFields.places,
|
|
44
|
+
StripeCouponCodeFields.expiresAt,
|
|
45
|
+
StripeCouponCodeFields.automatic,
|
|
46
|
+
StripeCouponCodeFields.status,
|
|
47
|
+
StripeCouponCodeFields.actions,
|
|
48
|
+
]}
|
|
49
|
+
tableGeneratorType={Modules.StripeCouponCode}
|
|
50
|
+
functions={functions}
|
|
51
|
+
title={t("billing.admin.coupons.codes.title")}
|
|
52
|
+
emptyState={
|
|
53
|
+
<EmptyState
|
|
54
|
+
icon={TicketIcon}
|
|
55
|
+
title={t("billing.admin.coupons.codes.empty.title")}
|
|
56
|
+
description={t("billing.admin.coupons.codes.empty.description")}
|
|
57
|
+
/>
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { TicketPercentIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ReactNode, useEffect } from "react";
|
|
6
|
+
import { ContentListTable } from "../../../../../components";
|
|
7
|
+
import { Modules } from "../../../../../core";
|
|
8
|
+
import { DataListRetriever, useDataListRetriever } from "../../../../../hooks";
|
|
9
|
+
import { EmptyState } from "../../../../../shadcnui";
|
|
10
|
+
import { useCouponContext } from "../../../contexts/CouponContext";
|
|
11
|
+
import { StripeCouponFields } from "../../data/stripe-coupon.fields";
|
|
12
|
+
import { StripeCouponInterface } from "../../data/stripe-coupon.interface";
|
|
13
|
+
import { StripeCouponService } from "../../data/stripe-coupon.service";
|
|
14
|
+
import CouponEditor from "../forms/CouponEditor";
|
|
15
|
+
|
|
16
|
+
type CouponsListProps = {
|
|
17
|
+
/**
|
|
18
|
+
* Pass this whenever the list is the page's own content inside a
|
|
19
|
+
* `RoundPageContainer fullWidth`. Without it ContentListTable wraps itself in
|
|
20
|
+
* `rounded-md border`, drawing a second bordered card inside the page's
|
|
21
|
+
* rounded shell.
|
|
22
|
+
*/
|
|
23
|
+
fullWidth?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function CouponsList({ fullWidth }: CouponsListProps = {}) {
|
|
27
|
+
const t = useTranslations();
|
|
28
|
+
const { catalogueVersion } = useCouponContext();
|
|
29
|
+
|
|
30
|
+
const data: DataListRetriever<StripeCouponInterface> = useDataListRetriever({
|
|
31
|
+
retriever: (params) => StripeCouponService.listCoupons(params),
|
|
32
|
+
retrieverParams: {},
|
|
33
|
+
module: Modules.StripeCoupon,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Renames and code changes happen on the detail page, so the list has to
|
|
37
|
+
// learn about them through the provider's counter rather than a direct callback.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (catalogueVersion > 0) void data.refresh();
|
|
40
|
+
}, [catalogueVersion]);
|
|
41
|
+
|
|
42
|
+
const functions: ReactNode[] = [<CouponEditor key="create-coupon" />];
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<ContentListTable
|
|
46
|
+
data={data}
|
|
47
|
+
fields={[
|
|
48
|
+
StripeCouponFields.name,
|
|
49
|
+
StripeCouponFields.discount,
|
|
50
|
+
StripeCouponFields.places,
|
|
51
|
+
StripeCouponFields.codes,
|
|
52
|
+
StripeCouponFields.status,
|
|
53
|
+
]}
|
|
54
|
+
tableGeneratorType={Modules.StripeCoupon}
|
|
55
|
+
functions={functions}
|
|
56
|
+
fullWidth={fullWidth}
|
|
57
|
+
title={t("billing.admin.coupons.title")}
|
|
58
|
+
emptyState={
|
|
59
|
+
<EmptyState
|
|
60
|
+
icon={TicketPercentIcon}
|
|
61
|
+
title={t("billing.admin.coupons.empty.title")}
|
|
62
|
+
description={t("billing.admin.coupons.empty.description")}
|
|
63
|
+
/>
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PowerIcon, RotateCcwIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { errorToast } from "../../../../../components/errors";
|
|
7
|
+
import { Button, ConfirmDialog, FieldLabel, Switch } from "../../../../../shadcnui";
|
|
8
|
+
import { useCouponContext } from "../../../contexts/CouponContext";
|
|
9
|
+
import { StripeCouponCodeInterface } from "../../data/stripe-coupon-code.interface";
|
|
10
|
+
import type { CouponCodeStatus } from "../../hooks/useStripeCouponCodeTableStructure";
|
|
11
|
+
|
|
12
|
+
type CouponCodeActionsProps = {
|
|
13
|
+
code: StripeCouponCodeInterface;
|
|
14
|
+
status: CouponCodeStatus;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Row controls of a code: the "Apply automatically" switch and the deactivate /
|
|
19
|
+
* reactivate button. The button sits behind ConfirmDialog, which is controlled
|
|
20
|
+
* and trigger-less by design — the caller owns the trigger button, so none of
|
|
21
|
+
* Base UI's trigger-composition pitfalls apply (no asChild, no nested <button>).
|
|
22
|
+
*/
|
|
23
|
+
export function CouponCodeActions({ code, status }: CouponCodeActionsProps) {
|
|
24
|
+
const t = useTranslations();
|
|
25
|
+
const { updateCodeFlags } = useCouponContext();
|
|
26
|
+
const [open, setOpen] = useState<boolean>(false);
|
|
27
|
+
const [pending, setPending] = useState<boolean>(false);
|
|
28
|
+
|
|
29
|
+
// An expired or used-up code stays dead: Stripe never revives it.
|
|
30
|
+
const isDead = status === "expired" || status === "used_up";
|
|
31
|
+
const isInactive = !code.active;
|
|
32
|
+
const scope = isInactive ? "reactivate" : "deactivate";
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex items-center justify-end gap-x-4">
|
|
36
|
+
{/* Typography role 9 (field label): FieldLabel wraps the switch, so a click on the text toggles it. */}
|
|
37
|
+
<FieldLabel>
|
|
38
|
+
<Switch
|
|
39
|
+
checked={code.automatic}
|
|
40
|
+
// Switching off is always allowed; switching on only for a live code.
|
|
41
|
+
disabled={pending || (!code.automatic && status !== "active")}
|
|
42
|
+
onCheckedChange={async (checked) => {
|
|
43
|
+
setPending(true);
|
|
44
|
+
try {
|
|
45
|
+
// The server clears the flag on every other code in the same
|
|
46
|
+
// write; the context's refreshCatalogue re-reads the list so the
|
|
47
|
+
// other row turns off too.
|
|
48
|
+
await updateCodeFlags({ id: code.id, automatic: checked });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
errorToast({ title: t("billing.admin.coupons.fields.automatic"), error });
|
|
51
|
+
} finally {
|
|
52
|
+
setPending(false);
|
|
53
|
+
}
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
{t("billing.admin.coupons.fields.automatic")}
|
|
57
|
+
</FieldLabel>
|
|
58
|
+
<Button variant="outline" size="sm" disabled={isDead} onClick={() => setOpen(true)}>
|
|
59
|
+
{isInactive ? <RotateCcwIcon /> : <PowerIcon />}
|
|
60
|
+
{t(`billing.admin.coupons.codes.${scope}.confirm`)}
|
|
61
|
+
</Button>
|
|
62
|
+
<ConfirmDialog
|
|
63
|
+
open={open}
|
|
64
|
+
onOpenChange={setOpen}
|
|
65
|
+
title={t(`billing.admin.coupons.codes.${scope}.title`)}
|
|
66
|
+
description={t(`billing.admin.coupons.codes.${scope}.description`, { code: code.code })}
|
|
67
|
+
confirmLabel={t(`billing.admin.coupons.codes.${scope}.confirm`)}
|
|
68
|
+
cancelLabel={t("ui.buttons.cancel")}
|
|
69
|
+
destructive={!isInactive}
|
|
70
|
+
onConfirm={async () => {
|
|
71
|
+
try {
|
|
72
|
+
await updateCodeFlags({ id: code.id, active: isInactive });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
errorToast({ title: t(`billing.admin.coupons.codes.${scope}.title`), error });
|
|
75
|
+
throw error; // keeps the dialog open — ConfirmDialog swallows rejections
|
|
76
|
+
}
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./CouponCodeActions";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { DataClassRegistry } from "../../../../../core/registry/DataClassRegistry";
|
|
3
|
+
import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
|
|
4
|
+
import { ModuleFactory, ModuleWithPermissions } from "../../../../../permissions/types";
|
|
5
|
+
import { StripeCouponCodeModule } from "../../stripe-coupon-code.module";
|
|
6
|
+
import { StripeCouponModule } from "../../stripe-coupon.module";
|
|
7
|
+
import { StripeCouponOfferModule } from "../../stripe-coupon-offer.module";
|
|
8
|
+
import { StripeCoupon } from "../stripe-coupon";
|
|
9
|
+
import { StripeCouponCode } from "../stripe-coupon-code";
|
|
10
|
+
|
|
11
|
+
// createJsonApi reads Modules.X.name (resolved lazily via ModuleRegistry), and
|
|
12
|
+
// _readIncluded rehydrates through the registered model, so the modules must be
|
|
13
|
+
// registered first — mirrors email-automation-models.spec.
|
|
14
|
+
const moduleFactory: ModuleFactory = (params) =>
|
|
15
|
+
({
|
|
16
|
+
pageUrl: params.pageUrl,
|
|
17
|
+
name: params.name,
|
|
18
|
+
model: params.model,
|
|
19
|
+
}) as ModuleWithPermissions;
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
const coupon = StripeCouponModule(moduleFactory);
|
|
23
|
+
const code = StripeCouponCodeModule(moduleFactory);
|
|
24
|
+
ModuleRegistry.register("StripeCoupon", coupon);
|
|
25
|
+
ModuleRegistry.register("StripeCouponCode", code);
|
|
26
|
+
ModuleRegistry.register("StripeCouponOffer", StripeCouponOfferModule(moduleFactory));
|
|
27
|
+
// The included code is rehydrated through the class registry (normally filled by bootstrap()).
|
|
28
|
+
DataClassRegistry.registerObjectClass(coupon, StripeCoupon);
|
|
29
|
+
DataClassRegistry.registerObjectClass(code, StripeCouponCode);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("stripe coupon models", () => {
|
|
33
|
+
it("rehydrates redeemBy into a Date and reads the included codes", () => {
|
|
34
|
+
const coupon = new StripeCoupon().rehydrate({
|
|
35
|
+
jsonApi: {
|
|
36
|
+
id: "co-1",
|
|
37
|
+
type: "stripe-coupons",
|
|
38
|
+
attributes: {
|
|
39
|
+
stripeCouponId: "co_1",
|
|
40
|
+
name: "Founding GM",
|
|
41
|
+
discountType: "percent_off",
|
|
42
|
+
percentOff: 20,
|
|
43
|
+
duration: "forever",
|
|
44
|
+
redeemBy: "2027-01-01T00:00:00.000Z",
|
|
45
|
+
maxRedemptions: 50,
|
|
46
|
+
timesRedeemed: 13,
|
|
47
|
+
valid: true,
|
|
48
|
+
},
|
|
49
|
+
relationships: { codes: { data: [{ type: "stripe-coupon-codes", id: "cc-1" }] } },
|
|
50
|
+
},
|
|
51
|
+
included: [
|
|
52
|
+
{
|
|
53
|
+
id: "cc-1",
|
|
54
|
+
type: "stripe-coupon-codes",
|
|
55
|
+
attributes: {
|
|
56
|
+
stripePromotionCodeId: "promo_1",
|
|
57
|
+
code: "FOUNDER",
|
|
58
|
+
active: true,
|
|
59
|
+
automatic: true,
|
|
60
|
+
expiresAt: "2027-02-01T00:00:00.000Z",
|
|
61
|
+
timesRedeemed: 13,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
} as any);
|
|
66
|
+
|
|
67
|
+
expect(coupon.redeemBy).toBeInstanceOf(Date);
|
|
68
|
+
expect(coupon.redeemBy?.toISOString()).toBe("2027-01-01T00:00:00.000Z");
|
|
69
|
+
expect(coupon.codes.length).toBe(1);
|
|
70
|
+
expect(coupon.codes[0].code).toBe("FOUNDER");
|
|
71
|
+
expect(coupon.codes[0].expiresAt).toBeInstanceOf(Date);
|
|
72
|
+
expect(coupon.timesRedeemed).toBe(13);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("serialises the datetimes of the coupon and its first code as ISO strings", () => {
|
|
76
|
+
const body = new StripeCoupon().createJsonApi({
|
|
77
|
+
id: "co-1",
|
|
78
|
+
name: "Founding GM",
|
|
79
|
+
discountType: "percent_off",
|
|
80
|
+
percentOff: 20,
|
|
81
|
+
duration: "forever",
|
|
82
|
+
productIds: ["p1"],
|
|
83
|
+
redeemBy: new Date("2027-01-01T00:00:00.000Z"),
|
|
84
|
+
maxRedemptions: 50,
|
|
85
|
+
firstCode: { code: "FOUNDER", automatic: true, expiresAt: new Date("2027-02-01T00:00:00.000Z") },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(body.data.type).toBe("stripe-coupons");
|
|
89
|
+
expect(body.data.attributes.redeemBy).toBe("2027-01-01T00:00:00.000Z");
|
|
90
|
+
expect(body.data.attributes.firstCode.expiresAt).toBe("2027-02-01T00:00:00.000Z");
|
|
91
|
+
expect(body.data.attributes.firstCode.automatic).toBe(true);
|
|
92
|
+
expect(body.data.attributes.productIds).toEqual(["p1"]);
|
|
93
|
+
expect("amountOff" in body.data.attributes).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("builds the rename body with the name only", () => {
|
|
97
|
+
expect(new StripeCoupon().createRenameJsonApi({ id: "co-1", name: "Renamed" })).toEqual({
|
|
98
|
+
data: { type: "stripe-coupons", id: "co-1", attributes: { name: "Renamed" } },
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("writes the code's coupon relationship", () => {
|
|
103
|
+
const body = new StripeCouponCode().createJsonApi({ id: "c1", couponId: "co-1", code: "SPRING" });
|
|
104
|
+
expect(body.data.type).toBe("stripe-coupon-codes");
|
|
105
|
+
expect(body.data.attributes).toEqual({ code: "SPRING" });
|
|
106
|
+
expect(body.data.relationships.coupon).toEqual({ data: { type: "stripe-coupons", id: "co-1" } });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("builds the flags body with only the flags that are set", () => {
|
|
110
|
+
const body = new StripeCouponCode().createFlagsJsonApi({ id: "c1", automatic: true });
|
|
111
|
+
expect(body).toEqual({ data: { type: "stripe-coupon-codes", id: "c1", attributes: { automatic: true } } });
|
|
112
|
+
expect("active" in body.data.attributes).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./stripe-coupon";
|
|
2
|
+
export * from "./stripe-coupon.fields";
|
|
3
|
+
export * from "./stripe-coupon.interface";
|
|
4
|
+
export * from "./stripe-coupon.service";
|
|
5
|
+
export * from "./stripe-coupon-code";
|
|
6
|
+
export * from "./stripe-coupon-code.fields";
|
|
7
|
+
export * from "./stripe-coupon-code.interface";
|
|
8
|
+
export * from "./stripe-coupon-code.service";
|
|
9
|
+
export * from "./stripe-coupon-offer";
|
|
10
|
+
export * from "./stripe-coupon-offer.interface";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../../core";
|
|
2
|
+
import { StripeCouponInterface } from "./stripe-coupon.interface";
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Stripe Coupon Code Interfaces (a customer-facing promotion code of a coupon)
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
export interface StripeCouponCodeInterface extends ApiDataInterface {
|
|
9
|
+
get stripePromotionCodeId(): string;
|
|
10
|
+
get code(): string;
|
|
11
|
+
get active(): boolean;
|
|
12
|
+
get automatic(): boolean;
|
|
13
|
+
get firstTimeTransaction(): boolean;
|
|
14
|
+
get minimumAmount(): number | undefined; // minor units
|
|
15
|
+
get minimumAmountCurrency(): string | undefined;
|
|
16
|
+
get stripeCustomerId(): string | undefined;
|
|
17
|
+
get expiresAt(): Date | undefined;
|
|
18
|
+
get maxRedemptions(): number | undefined;
|
|
19
|
+
get timesRedeemed(): number;
|
|
20
|
+
get coupon(): StripeCouponInterface | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Stripe Coupon Code Input DTOs
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export type StripeCouponCodeInput = {
|
|
28
|
+
id: string;
|
|
29
|
+
couponId: string;
|
|
30
|
+
code?: string;
|
|
31
|
+
firstTimeTransaction?: boolean;
|
|
32
|
+
minimumAmount?: number;
|
|
33
|
+
minimumAmountCurrency?: string;
|
|
34
|
+
stripeCustomerId?: string;
|
|
35
|
+
expiresAt?: Date;
|
|
36
|
+
maxRedemptions?: number;
|
|
37
|
+
automatic?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type StripeCouponCodeFlagsInput = {
|
|
41
|
+
id: string;
|
|
42
|
+
active?: boolean;
|
|
43
|
+
automatic?: boolean;
|
|
44
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef, PreviousRef } from "../../../../core";
|
|
2
|
+
import { STRIPE_COUPON_CODE_AUTOMATIC_SEGMENT } from "../stripe-coupon-code.module";
|
|
3
|
+
import { StripeCouponCode } from "./stripe-coupon-code";
|
|
4
|
+
import {
|
|
5
|
+
StripeCouponCodeFlagsInput,
|
|
6
|
+
StripeCouponCodeInput,
|
|
7
|
+
StripeCouponCodeInterface,
|
|
8
|
+
} from "./stripe-coupon-code.interface";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Billing service for the customer-facing codes of a coupon
|
|
12
|
+
*/
|
|
13
|
+
export class StripeCouponCodeService extends AbstractService {
|
|
14
|
+
/**
|
|
15
|
+
* List the codes of a coupon (admin)
|
|
16
|
+
*/
|
|
17
|
+
static async listByCoupon(params: {
|
|
18
|
+
couponId: string;
|
|
19
|
+
next?: NextRef;
|
|
20
|
+
prev?: PreviousRef;
|
|
21
|
+
}): Promise<StripeCouponCodeInterface[]> {
|
|
22
|
+
const endpoint = new EndpointCreator({
|
|
23
|
+
endpoint: Modules.StripeCoupon,
|
|
24
|
+
id: params.couponId,
|
|
25
|
+
childEndpoint: Modules.StripeCouponCode,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return this.callApi({
|
|
29
|
+
type: Modules.StripeCouponCode,
|
|
30
|
+
method: HttpMethod.GET,
|
|
31
|
+
endpoint: endpoint.generate(),
|
|
32
|
+
next: params.next,
|
|
33
|
+
previous: params.prev,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a code on an existing coupon (admin)
|
|
39
|
+
*/
|
|
40
|
+
static async createCode(params: StripeCouponCodeInput): Promise<StripeCouponCodeInterface> {
|
|
41
|
+
const endpoint = new EndpointCreator({
|
|
42
|
+
endpoint: Modules.StripeCouponCode,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return this.callApi<StripeCouponCodeInterface>({
|
|
46
|
+
type: Modules.StripeCouponCode,
|
|
47
|
+
method: HttpMethod.POST,
|
|
48
|
+
endpoint: endpoint.generate(),
|
|
49
|
+
input: params,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Switch "Apply automatically" and / or deactivate / reactivate a code (admin)
|
|
55
|
+
*/
|
|
56
|
+
static async updateFlags(params: StripeCouponCodeFlagsInput): Promise<StripeCouponCodeInterface> {
|
|
57
|
+
const endpoint = new EndpointCreator({
|
|
58
|
+
endpoint: Modules.StripeCouponCode,
|
|
59
|
+
id: params.id,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return this.callApi<StripeCouponCodeInterface>({
|
|
63
|
+
type: Modules.StripeCouponCode,
|
|
64
|
+
method: HttpMethod.PATCH,
|
|
65
|
+
endpoint: endpoint.generate(),
|
|
66
|
+
input: new StripeCouponCode().createFlagsJsonApi(params),
|
|
67
|
+
overridesJsonApiCreation: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The code the server would apply automatically for this price at a first
|
|
73
|
+
* payment, or null when none applies (logged-in company administrator)
|
|
74
|
+
*/
|
|
75
|
+
static async findAutomatic(params: { priceId: string }): Promise<StripeCouponCodeInterface | null> {
|
|
76
|
+
const endpoint = new EndpointCreator({
|
|
77
|
+
endpoint: Modules.StripeCouponCode,
|
|
78
|
+
id: STRIPE_COUPON_CODE_AUTOMATIC_SEGMENT,
|
|
79
|
+
});
|
|
80
|
+
endpoint.addAdditionalParam("priceId", params.priceId);
|
|
81
|
+
|
|
82
|
+
const codes = await this.callApi<StripeCouponCodeInterface[]>({
|
|
83
|
+
type: Modules.StripeCouponCode,
|
|
84
|
+
method: HttpMethod.GET,
|
|
85
|
+
endpoint: endpoint.generate(),
|
|
86
|
+
// A failed lookup must reach the wizard, which then refuses to show full price as the truth
|
|
87
|
+
suppressGlobalError: true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return codes?.[0] ?? null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../../core";
|
|
2
|
+
import {
|
|
3
|
+
StripeCouponCodeFlagsInput,
|
|
4
|
+
StripeCouponCodeInput,
|
|
5
|
+
StripeCouponCodeInterface,
|
|
6
|
+
} from "./stripe-coupon-code.interface";
|
|
7
|
+
import { StripeCouponInterface } from "./stripe-coupon.interface";
|
|
8
|
+
import { StripeCoupon } from "./stripe-coupon";
|
|
9
|
+
|
|
10
|
+
export class StripeCouponCode extends AbstractApiData implements StripeCouponCodeInterface {
|
|
11
|
+
private _stripePromotionCodeId?: string;
|
|
12
|
+
private _code: string = "";
|
|
13
|
+
private _active: boolean = true;
|
|
14
|
+
private _automatic: boolean = false;
|
|
15
|
+
private _firstTimeTransaction: boolean = false;
|
|
16
|
+
private _minimumAmount?: number;
|
|
17
|
+
private _minimumAmountCurrency?: string;
|
|
18
|
+
private _stripeCustomerId?: string;
|
|
19
|
+
private _expiresAt?: Date;
|
|
20
|
+
private _maxRedemptions?: number;
|
|
21
|
+
private _timesRedeemed: number = 0;
|
|
22
|
+
private _coupon?: StripeCouponInterface;
|
|
23
|
+
|
|
24
|
+
get stripePromotionCodeId(): string {
|
|
25
|
+
if (!this._stripePromotionCodeId) throw new Error("stripePromotionCodeId is not defined");
|
|
26
|
+
return this._stripePromotionCodeId;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get code(): string {
|
|
30
|
+
return this._code;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get active(): boolean {
|
|
34
|
+
return this._active;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get automatic(): boolean {
|
|
38
|
+
return this._automatic;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get firstTimeTransaction(): boolean {
|
|
42
|
+
return this._firstTimeTransaction;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get minimumAmount(): number | undefined {
|
|
46
|
+
return this._minimumAmount;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get minimumAmountCurrency(): string | undefined {
|
|
50
|
+
return this._minimumAmountCurrency;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get stripeCustomerId(): string | undefined {
|
|
54
|
+
return this._stripeCustomerId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get expiresAt(): Date | undefined {
|
|
58
|
+
return this._expiresAt;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get maxRedemptions(): number | undefined {
|
|
62
|
+
return this._maxRedemptions;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get timesRedeemed(): number {
|
|
66
|
+
return this._timesRedeemed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get coupon(): StripeCouponInterface | undefined {
|
|
70
|
+
return this._coupon;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
74
|
+
super.rehydrate(data);
|
|
75
|
+
|
|
76
|
+
const attributes = data.jsonApi.attributes;
|
|
77
|
+
|
|
78
|
+
this._stripePromotionCodeId = attributes.stripePromotionCodeId;
|
|
79
|
+
this._code = attributes.code ?? "";
|
|
80
|
+
this._active = attributes.active ?? true;
|
|
81
|
+
this._automatic = attributes.automatic ?? false;
|
|
82
|
+
this._firstTimeTransaction = attributes.firstTimeTransaction ?? false;
|
|
83
|
+
this._minimumAmount = attributes.minimumAmount ?? undefined;
|
|
84
|
+
this._minimumAmountCurrency = attributes.minimumAmountCurrency ?? undefined;
|
|
85
|
+
this._stripeCustomerId = attributes.stripeCustomerId ?? undefined;
|
|
86
|
+
this._expiresAt = attributes.expiresAt ? new Date(attributes.expiresAt) : undefined;
|
|
87
|
+
this._maxRedemptions = attributes.maxRedemptions ?? undefined;
|
|
88
|
+
this._timesRedeemed = attributes.timesRedeemed ?? 0;
|
|
89
|
+
|
|
90
|
+
this._coupon = this._readIncluded(data, "coupon", Modules.StripeCoupon) as StripeCouponInterface | undefined;
|
|
91
|
+
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
createJsonApi(data: StripeCouponCodeInput): any {
|
|
96
|
+
const { id, couponId, ...code } = data;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
data: {
|
|
100
|
+
type: Modules.StripeCouponCode.name,
|
|
101
|
+
id,
|
|
102
|
+
attributes: StripeCoupon.codeAttributes(code),
|
|
103
|
+
relationships: {
|
|
104
|
+
coupon: {
|
|
105
|
+
data: {
|
|
106
|
+
type: Modules.StripeCoupon.name,
|
|
107
|
+
id: couponId,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* PATCH body for the flags a code can still change after creation.
|
|
117
|
+
* Only the flags that are set are sent, so switching one never touches the other.
|
|
118
|
+
*/
|
|
119
|
+
createFlagsJsonApi(data: StripeCouponCodeFlagsInput): any {
|
|
120
|
+
const attributes: Record<string, boolean> = {};
|
|
121
|
+
if (data.active !== undefined) attributes.active = data.active;
|
|
122
|
+
if (data.automatic !== undefined) attributes.automatic = data.automatic;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
data: {
|
|
126
|
+
type: Modules.StripeCouponCode.name,
|
|
127
|
+
id: data.id,
|
|
128
|
+
attributes,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../../core";
|
|
2
|
+
import { StripeCouponDiscountType, StripeCouponDurationType } from "./stripe-coupon.interface";
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Stripe Coupon Offer Interface (public, synthetic: the automatic code's places left)
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
export interface StripeCouponOfferInterface extends ApiDataInterface {
|
|
9
|
+
get discountType(): StripeCouponDiscountType;
|
|
10
|
+
get percentOff(): number | undefined;
|
|
11
|
+
get amountOff(): number | undefined; // minor units
|
|
12
|
+
get currency(): string | undefined;
|
|
13
|
+
get duration(): StripeCouponDurationType;
|
|
14
|
+
get durationInMonths(): number | undefined;
|
|
15
|
+
get placesLeft(): number;
|
|
16
|
+
get placesTotal(): number;
|
|
17
|
+
}
|