@carlonicora/nextjs-jsonapi 5.6.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-NEYG5L6W.mjs → BlockNoteEditor-R3WXUFHX.mjs} +4 -4
- package/dist/{BlockNoteEditor-AE3ZTEEH.js → BlockNoteEditor-ZHXXFWUJ.js} +19 -19
- package/dist/{BlockNoteEditor-AE3ZTEEH.js.map → BlockNoteEditor-ZHXXFWUJ.js.map} +1 -1
- package/dist/{BlockNoteViewer-3OUT5UFY.mjs → BlockNoteViewer-BVVN6D3T.mjs} +2 -2
- package/dist/{BlockNoteViewer-4CFKQM7U.js → BlockNoteViewer-SD3ZFIXD.js} +3 -3
- package/dist/{BlockNoteViewer-4CFKQM7U.js.map → BlockNoteViewer-SD3ZFIXD.js.map} +1 -1
- package/dist/billing/index.d.mts +33 -43
- package/dist/billing/index.d.ts +33 -43
- package/dist/billing/index.js +590 -574
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +28 -12
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-3I5AAJXV.js → chunk-2TXE57LE.js} +3 -3
- package/dist/{chunk-3I5AAJXV.js.map → chunk-2TXE57LE.js.map} +1 -1
- package/dist/{chunk-HKZAESS6.js → chunk-5TH2DSAH.js} +17 -12
- package/dist/chunk-5TH2DSAH.js.map +1 -0
- package/dist/{chunk-SFYPXCK4.mjs → chunk-CFB6FHIY.mjs} +638 -536
- package/dist/{chunk-SFYPXCK4.mjs.map → chunk-CFB6FHIY.mjs.map} +1 -1
- package/dist/{chunk-EYA2F6AN.mjs → chunk-FK26NX3S.mjs} +16 -11
- package/dist/chunk-FK26NX3S.mjs.map +1 -0
- package/dist/{chunk-53BAULDP.js → chunk-IJME5SAM.js} +4 -4
- package/dist/{chunk-53BAULDP.js.map → chunk-IJME5SAM.js.map} +1 -1
- package/dist/{chunk-KQHBONL4.mjs → chunk-OKCV4GPJ.mjs} +2 -2
- package/dist/{chunk-BTPUYUQ3.mjs → chunk-RGQYFSJF.mjs} +2 -2
- package/dist/{chunk-Y7RR57EW.mjs → chunk-WCHLCOZG.mjs} +2 -2
- package/dist/{chunk-RL3PL3KV.js → chunk-XOSC3FFP.js} +8 -8
- package/dist/{chunk-RL3PL3KV.js.map → chunk-XOSC3FFP.js.map} +1 -1
- package/dist/{chunk-LQ4DK4JF.js → chunk-ZCOPOO7T.js} +1314 -1212
- 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 +9 -3
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +10 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +9 -1
- package/dist/{format-options-D6c7FNDi.d.mts → currency-DT_dveBx.d.mts} +41 -8
- package/dist/{format-options-BtX3o0Yu.d.ts → currency-pK9eRYRC.d.ts} +41 -8
- 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 +11 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/package.json +1 -1
- package/src/core/index.ts +3 -0
- package/src/features/billing/contexts/CouponContext.tsx +0 -2
- package/src/features/billing/stripe-coupon/components/details/CouponDetails.tsx +5 -2
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx +6 -5
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeFields.tsx +67 -45
- package/src/features/billing/stripe-coupon/components/forms/CouponEditor.tsx +304 -212
- package/src/features/billing/stripe-coupon/components/widgets/CouponCodeActions.tsx +4 -3
- package/src/features/billing/stripe-coupon/data/stripe-coupon.interface.ts +11 -7
- package/src/features/billing/stripe-coupon/data/stripe-coupon.ts +6 -4
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +14 -7
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +2 -2
- package/src/features/billing/stripe-coupon/stripe-coupon-code.module.ts +3 -2
- package/src/features/billing/stripe-coupon/stripe-coupon.module.ts +11 -4
- package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +1 -1
- package/src/features/billing/stripe-subscription/hooks/wizard-promotion.ts +15 -8
- package/dist/chunk-EYA2F6AN.mjs.map +0 -1
- package/dist/chunk-HKZAESS6.js.map +0 -1
- package/dist/chunk-LQ4DK4JF.js.map +0 -1
- /package/dist/{BlockNoteEditor-NEYG5L6W.mjs.map → BlockNoteEditor-R3WXUFHX.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-3OUT5UFY.mjs.map → BlockNoteViewer-BVVN6D3T.mjs.map} +0 -0
- /package/dist/{chunk-KQHBONL4.mjs.map → chunk-OKCV4GPJ.mjs.map} +0 -0
- /package/dist/{chunk-BTPUYUQ3.mjs.map → chunk-RGQYFSJF.mjs.map} +0 -0
- /package/dist/{chunk-Y7RR57EW.mjs.map → chunk-WCHLCOZG.mjs.map} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
4
|
import { useTranslations } from "next-intl";
|
|
5
|
-
import { ReactNode, useCallback, useMemo } from "react";
|
|
5
|
+
import { ReactNode, useCallback, useMemo, useState } from "react";
|
|
6
6
|
import { useForm } from "react-hook-form";
|
|
7
7
|
import { v4 } from "uuid";
|
|
8
8
|
import { z } from "zod";
|
|
@@ -21,7 +21,13 @@ import { formatDate, useBillingFormatOptions } from "../../../components/utils";
|
|
|
21
21
|
import { useCouponContext } from "../../../contexts/CouponContext";
|
|
22
22
|
import { StripeProductInterface } from "../../../stripe-product/data/stripe-product.interface";
|
|
23
23
|
import { StripeProductService } from "../../../stripe-product/data/stripe-product.service";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
StripeCouponDiscountInput,
|
|
26
|
+
StripeCouponDurationInput,
|
|
27
|
+
StripeCouponInput,
|
|
28
|
+
StripeCouponInterface,
|
|
29
|
+
} from "../../data/stripe-coupon.interface";
|
|
30
|
+
import { STRIPE_COUPON_DETAIL_PAGE_URL } from "../../stripe-coupon.module";
|
|
25
31
|
import { formatCouponDiscount } from "../../utils/format-coupon-discount";
|
|
26
32
|
import {
|
|
27
33
|
CouponCodeFields,
|
|
@@ -31,6 +37,8 @@ import {
|
|
|
31
37
|
optionalDate,
|
|
32
38
|
optionalDecimal,
|
|
33
39
|
optionalPositiveInt,
|
|
40
|
+
parseOptionalDecimal,
|
|
41
|
+
parseOptionalPositiveInt,
|
|
34
42
|
toCouponCodeInput,
|
|
35
43
|
} from "./CouponCodeFields";
|
|
36
44
|
|
|
@@ -45,7 +53,115 @@ export type CouponEditorProps = {
|
|
|
45
53
|
onDialogOpenChange?: (open: boolean) => void;
|
|
46
54
|
};
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
type Translator = (key: string) => string;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Edit mode is a rename: Stripe allows changing only a coupon's name after
|
|
60
|
+
* creation, so the schema is the name alone.
|
|
61
|
+
*/
|
|
62
|
+
const couponRenameSchema = (t: Translator) =>
|
|
63
|
+
z.object({ id: z.string(), name: z.string().min(1, { message: t("billing.admin.coupons.errors.name") }) });
|
|
64
|
+
|
|
65
|
+
type CouponRenameValues = z.infer<ReturnType<typeof couponRenameSchema>>;
|
|
66
|
+
|
|
67
|
+
/** Mirrors StripeCouponPostAttributesDTO. */
|
|
68
|
+
const couponCreateSchema = (t: Translator) =>
|
|
69
|
+
z
|
|
70
|
+
.object({
|
|
71
|
+
id: z.uuidv4(),
|
|
72
|
+
name: z.string().min(1, { message: t("billing.admin.coupons.errors.name") }),
|
|
73
|
+
stripeCouponId: z.string().optional(),
|
|
74
|
+
discountType: z.enum(["percent_off", "amount_off"]),
|
|
75
|
+
percentOff: optionalDecimal(),
|
|
76
|
+
amountOff: optionalDecimal(),
|
|
77
|
+
currency: z.string().min(1),
|
|
78
|
+
duration: z.enum(["once", "repeating", "forever"]),
|
|
79
|
+
durationInMonths: optionalPositiveInt(),
|
|
80
|
+
products: z.array(z.object({ id: z.string(), name: z.string().optional() })),
|
|
81
|
+
redeemBy: optionalDate(),
|
|
82
|
+
maxRedemptions: optionalPositiveInt(),
|
|
83
|
+
firstCode: couponCodeSchema(t),
|
|
84
|
+
})
|
|
85
|
+
.superRefine((values, ctx) => {
|
|
86
|
+
const percentOff = parseOptionalDecimal(values.percentOff);
|
|
87
|
+
if (values.discountType === "percent_off" && (percentOff === undefined || percentOff < 0.01 || percentOff > 100))
|
|
88
|
+
ctx.addIssue({
|
|
89
|
+
code: "custom",
|
|
90
|
+
path: ["percentOff"],
|
|
91
|
+
message: t("billing.admin.coupons.errors.percentOff"),
|
|
92
|
+
});
|
|
93
|
+
const amountOff = parseOptionalDecimal(values.amountOff);
|
|
94
|
+
if (values.discountType === "amount_off" && (amountOff === undefined || amountOff <= 0))
|
|
95
|
+
ctx.addIssue({
|
|
96
|
+
code: "custom",
|
|
97
|
+
path: ["amountOff"],
|
|
98
|
+
message: t("billing.admin.coupons.errors.amountOff"),
|
|
99
|
+
});
|
|
100
|
+
if (values.duration === "repeating" && parseOptionalPositiveInt(values.durationInMonths) === undefined)
|
|
101
|
+
ctx.addIssue({
|
|
102
|
+
code: "custom",
|
|
103
|
+
path: ["durationInMonths"],
|
|
104
|
+
message: t("billing.admin.coupons.errors.durationInMonths"),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
type CouponCreateValues = z.infer<ReturnType<typeof couponCreateSchema>>;
|
|
109
|
+
|
|
110
|
+
/** The schema guarantees the value; a missing one is a bug, not user input. */
|
|
111
|
+
function required<T>(value: T | undefined, field: string): T {
|
|
112
|
+
if (value === undefined) throw new Error(`${field} is not set`);
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toDiscountInput(values: CouponCreateValues): StripeCouponDiscountInput {
|
|
117
|
+
if (values.discountType === "percent_off")
|
|
118
|
+
return { discountType: "percent_off", percentOff: required(parseOptionalDecimal(values.percentOff), "percentOff") };
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
discountType: "amount_off",
|
|
122
|
+
amountOff: Math.round(required(parseOptionalDecimal(values.amountOff), "amountOff") * 100),
|
|
123
|
+
currency: values.currency,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function toDurationInput(values: CouponCreateValues): StripeCouponDurationInput {
|
|
128
|
+
if (values.duration === "repeating")
|
|
129
|
+
return {
|
|
130
|
+
duration: "repeating",
|
|
131
|
+
durationInMonths: required(parseOptionalPositiveInt(values.durationInMonths), "durationInMonths"),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return { duration: values.duration };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Form values → the coupon the model serialises. Unset fields are left out. */
|
|
138
|
+
function toCouponInput(values: CouponCreateValues): StripeCouponInput {
|
|
139
|
+
const payload: StripeCouponInput = {
|
|
140
|
+
id: values.id,
|
|
141
|
+
name: values.name,
|
|
142
|
+
firstCode: toCouponCodeInput(values.firstCode),
|
|
143
|
+
...toDiscountInput(values),
|
|
144
|
+
...toDurationInput(values),
|
|
145
|
+
};
|
|
146
|
+
if (values.stripeCouponId?.trim()) payload.stripeCouponId = values.stripeCouponId.trim();
|
|
147
|
+
if (values.products.length > 0) payload.productIds = values.products.map((product) => product.id);
|
|
148
|
+
if (values.redeemBy) payload.redeemBy = values.redeemBy;
|
|
149
|
+
const maxRedemptions = parseOptionalPositiveInt(values.maxRedemptions);
|
|
150
|
+
if (maxRedemptions !== undefined) payload.maxRedemptions = maxRedemptions;
|
|
151
|
+
return payload;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The module with the coupon's own route as pageUrl: the module's pageUrl is the
|
|
156
|
+
* products page (the coupons list is its Codes tab), so EditorSheet's
|
|
157
|
+
* navigate / revalidate after a save would otherwise land on
|
|
158
|
+
* `/administration/products/<couponId>`.
|
|
159
|
+
*/
|
|
160
|
+
function couponPageModule() {
|
|
161
|
+
return { ...Modules.StripeCoupon, pageUrl: STRIPE_COUPON_DETAIL_PAGE_URL };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function CouponRenameEditor({
|
|
49
165
|
coupon,
|
|
50
166
|
propagateChanges,
|
|
51
167
|
onSuccess,
|
|
@@ -54,85 +170,116 @@ function CouponEditorInternal({
|
|
|
54
170
|
onClose,
|
|
55
171
|
dialogOpen,
|
|
56
172
|
onDialogOpenChange,
|
|
57
|
-
}: CouponEditorProps) {
|
|
173
|
+
}: CouponEditorProps & { coupon: StripeCouponInterface }) {
|
|
58
174
|
const t = useTranslations();
|
|
59
175
|
const fmt = useBillingFormatOptions();
|
|
60
176
|
const router = useI18nRouter();
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
177
|
+
const { renameCoupon } = useCouponContext();
|
|
178
|
+
|
|
179
|
+
const formSchema = useMemo(() => couponRenameSchema(t), [t]);
|
|
180
|
+
|
|
181
|
+
const getDefaultValues = useCallback((): CouponRenameValues => ({ id: coupon.id, name: coupon.name }), [coupon]);
|
|
182
|
+
|
|
183
|
+
const form = useForm<CouponRenameValues>({
|
|
184
|
+
resolver: zodResolver(formSchema),
|
|
185
|
+
defaultValues: getDefaultValues(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<EditorSheet
|
|
190
|
+
form={form}
|
|
191
|
+
entityType={t("billing.admin.coupons.entity")}
|
|
192
|
+
entityName={coupon.name}
|
|
193
|
+
isEdit
|
|
194
|
+
module={couponPageModule()}
|
|
195
|
+
size="lg"
|
|
196
|
+
propagateChanges={propagateChanges}
|
|
197
|
+
onSuccess={onSuccess}
|
|
198
|
+
onNavigate={(url) => router.push(url)}
|
|
199
|
+
onSubmit={async (values) => await renameCoupon({ id: coupon.id, name: values.name })}
|
|
200
|
+
onReset={getDefaultValues}
|
|
201
|
+
trigger={trigger}
|
|
202
|
+
forceShow={forceShow}
|
|
203
|
+
onClose={onClose}
|
|
204
|
+
dialogOpen={dialogOpen}
|
|
205
|
+
onDialogOpenChange={onDialogOpenChange}
|
|
206
|
+
>
|
|
207
|
+
<div className="flex w-full flex-col gap-y-4">
|
|
208
|
+
<FormInput
|
|
209
|
+
form={form}
|
|
210
|
+
id="name"
|
|
211
|
+
name={t("billing.admin.coupons.fields.name")}
|
|
212
|
+
placeholder={t("billing.admin.coupons.placeholders.name")}
|
|
213
|
+
isRequired
|
|
214
|
+
/>
|
|
215
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
216
|
+
<DetailField
|
|
217
|
+
label={t("billing.admin.coupons.fields.discount")}
|
|
218
|
+
value={<span className="tabular-nums">{formatCouponDiscount(coupon, t, fmt)}</span>}
|
|
219
|
+
/>
|
|
220
|
+
<DetailField
|
|
221
|
+
label={t("billing.admin.coupons.fields.redeemBy")}
|
|
222
|
+
value={<span className="tabular-nums">{coupon.redeemBy ? formatDate(coupon.redeemBy, fmt) : "—"}</span>}
|
|
223
|
+
/>
|
|
224
|
+
<DetailField
|
|
225
|
+
label={t("billing.admin.coupons.fields.maxRedemptions")}
|
|
226
|
+
value={
|
|
227
|
+
<span className="tabular-nums">
|
|
228
|
+
{coupon.maxRedemptions ?? t("billing.admin.coupons.places.no_limit")}
|
|
229
|
+
</span>
|
|
230
|
+
}
|
|
231
|
+
/>
|
|
232
|
+
<DetailField
|
|
233
|
+
label={t("billing.admin.coupons.fields.stripeId")}
|
|
234
|
+
value={<span className="tabular-nums">{coupon.stripeCouponId}</span>}
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
{/* Typography role 10 (helper text). */}
|
|
238
|
+
<p className="text-muted-foreground text-xs/relaxed">{t("billing.admin.coupons.fixed_notice")}</p>
|
|
239
|
+
</div>
|
|
240
|
+
</EditorSheet>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function CouponCreateEditor({
|
|
245
|
+
propagateChanges,
|
|
246
|
+
onSuccess,
|
|
247
|
+
trigger,
|
|
248
|
+
forceShow,
|
|
249
|
+
onClose,
|
|
250
|
+
dialogOpen,
|
|
251
|
+
onDialogOpenChange,
|
|
252
|
+
}: Omit<CouponEditorProps, "coupon">) {
|
|
253
|
+
const t = useTranslations();
|
|
254
|
+
const router = useI18nRouter();
|
|
255
|
+
const { createCoupon } = useCouponContext();
|
|
256
|
+
// Read once on mount: render stays pure.
|
|
257
|
+
const [now] = useState(() => new Date());
|
|
258
|
+
|
|
259
|
+
const formSchema = useMemo(() => couponCreateSchema(t), [t]);
|
|
110
260
|
|
|
111
261
|
const getDefaultValues = useCallback(
|
|
112
|
-
():
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
firstCode: couponCodeDefaults(),
|
|
130
|
-
},
|
|
131
|
-
[coupon],
|
|
262
|
+
(): CouponCreateValues => ({
|
|
263
|
+
id: v4(),
|
|
264
|
+
name: "",
|
|
265
|
+
stripeCouponId: "",
|
|
266
|
+
discountType: "percent_off",
|
|
267
|
+
percentOff: "",
|
|
268
|
+
amountOff: "",
|
|
269
|
+
currency: "eur",
|
|
270
|
+
duration: "forever",
|
|
271
|
+
// "" not undefined for every FormInput: see couponCodeDefaults.
|
|
272
|
+
durationInMonths: "",
|
|
273
|
+
products: [],
|
|
274
|
+
redeemBy: undefined,
|
|
275
|
+
maxRedemptions: "",
|
|
276
|
+
firstCode: couponCodeDefaults(),
|
|
277
|
+
}),
|
|
278
|
+
[],
|
|
132
279
|
);
|
|
133
280
|
|
|
134
|
-
const form = useForm<
|
|
135
|
-
resolver: zodResolver(formSchema
|
|
281
|
+
const form = useForm<CouponCreateValues>({
|
|
282
|
+
resolver: zodResolver(formSchema),
|
|
136
283
|
defaultValues: getDefaultValues(),
|
|
137
284
|
});
|
|
138
285
|
|
|
@@ -154,36 +301,13 @@ function CouponEditorInternal({
|
|
|
154
301
|
<EditorSheet
|
|
155
302
|
form={form}
|
|
156
303
|
entityType={t("billing.admin.coupons.entity")}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
module={Modules.StripeCoupon}
|
|
304
|
+
isEdit={false}
|
|
305
|
+
module={couponPageModule()}
|
|
160
306
|
size="lg"
|
|
161
307
|
propagateChanges={propagateChanges}
|
|
162
308
|
onSuccess={onSuccess}
|
|
163
309
|
onNavigate={(url) => router.push(url)}
|
|
164
|
-
onSubmit={async (values
|
|
165
|
-
if (isEdit) return await renameCoupon({ id: coupon.id, name: values.name });
|
|
166
|
-
|
|
167
|
-
const payload: StripeCouponInput = {
|
|
168
|
-
id: values.id,
|
|
169
|
-
name: values.name,
|
|
170
|
-
discountType: values.discountType,
|
|
171
|
-
duration: values.duration,
|
|
172
|
-
firstCode: toCouponCodeInput(values.firstCode),
|
|
173
|
-
};
|
|
174
|
-
if (values.stripeCouponId?.trim()) payload.stripeCouponId = values.stripeCouponId.trim();
|
|
175
|
-
if (values.discountType === "percent_off") payload.percentOff = values.percentOff;
|
|
176
|
-
else {
|
|
177
|
-
payload.amountOff = Math.round(values.amountOff * 100);
|
|
178
|
-
payload.currency = values.currency;
|
|
179
|
-
}
|
|
180
|
-
if (values.duration === "repeating") payload.durationInMonths = values.durationInMonths;
|
|
181
|
-
if (values.products.length > 0) payload.productIds = values.products.map((p: { id: string }) => p.id);
|
|
182
|
-
if (values.redeemBy) payload.redeemBy = values.redeemBy;
|
|
183
|
-
if (values.maxRedemptions !== undefined) payload.maxRedemptions = values.maxRedemptions;
|
|
184
|
-
|
|
185
|
-
return await createCoupon(payload);
|
|
186
|
-
}}
|
|
310
|
+
onSubmit={async (values) => await createCoupon(toCouponInput(values))}
|
|
187
311
|
onReset={getDefaultValues}
|
|
188
312
|
trigger={trigger}
|
|
189
313
|
forceShow={forceShow}
|
|
@@ -199,135 +323,103 @@ function CouponEditorInternal({
|
|
|
199
323
|
placeholder={t("billing.admin.coupons.placeholders.name")}
|
|
200
324
|
isRequired
|
|
201
325
|
/>
|
|
326
|
+
<FormInput
|
|
327
|
+
form={form}
|
|
328
|
+
id="stripeCouponId"
|
|
329
|
+
name={t("billing.admin.coupons.fields.stripeCouponId")}
|
|
330
|
+
placeholder={t("billing.admin.coupons.placeholders.stripeCouponId")}
|
|
331
|
+
/>
|
|
202
332
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
</span>
|
|
220
|
-
}
|
|
221
|
-
/>
|
|
222
|
-
<DetailField
|
|
223
|
-
label={t("billing.admin.coupons.fields.stripeId")}
|
|
224
|
-
value={<span className="tabular-nums">{coupon.stripeCouponId}</span>}
|
|
225
|
-
/>
|
|
226
|
-
</div>
|
|
227
|
-
{/* Typography role 10 (helper text). */}
|
|
228
|
-
<p className="text-muted-foreground text-xs/relaxed">{t("billing.admin.coupons.fixed_notice")}</p>
|
|
229
|
-
</>
|
|
333
|
+
<FormSelect
|
|
334
|
+
form={form}
|
|
335
|
+
id="discountType"
|
|
336
|
+
name={t("billing.admin.coupons.fields.discountType")}
|
|
337
|
+
values={discountTypeOptions}
|
|
338
|
+
/>
|
|
339
|
+
|
|
340
|
+
{watchDiscountType === "percent_off" ? (
|
|
341
|
+
<FormInput
|
|
342
|
+
form={form}
|
|
343
|
+
id="percentOff"
|
|
344
|
+
type="decimal"
|
|
345
|
+
name={t("billing.admin.coupons.fields.percentOff")}
|
|
346
|
+
placeholder={t("billing.admin.coupons.placeholders.percentOff")}
|
|
347
|
+
isRequired
|
|
348
|
+
/>
|
|
230
349
|
) : (
|
|
231
|
-
|
|
350
|
+
<div className="grid grid-cols-2 gap-x-4">
|
|
351
|
+
{/* type="decimal", NOT type="currency": the currency variant hardcodes
|
|
352
|
+
a Euro symbol (FormInput.tsx:105-106), which contradicts the
|
|
353
|
+
currency selector sitting next to it. */}
|
|
232
354
|
<FormInput
|
|
233
355
|
form={form}
|
|
234
|
-
id="
|
|
235
|
-
|
|
236
|
-
|
|
356
|
+
id="amountOff"
|
|
357
|
+
type="decimal"
|
|
358
|
+
name={t("billing.admin.coupons.fields.amountOff")}
|
|
359
|
+
placeholder={t("billing.admin.coupons.placeholders.amountOff")}
|
|
360
|
+
isRequired
|
|
237
361
|
/>
|
|
238
|
-
|
|
239
362
|
<FormSelect
|
|
240
363
|
form={form}
|
|
241
|
-
id="
|
|
242
|
-
name={t("billing.admin.coupons.fields.
|
|
243
|
-
values={
|
|
364
|
+
id="currency"
|
|
365
|
+
name={t("billing.admin.coupons.fields.currency")}
|
|
366
|
+
values={couponCurrencyOptions}
|
|
367
|
+
isRequired
|
|
244
368
|
/>
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
245
371
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
placeholder={t("billing.admin.coupons.placeholders.percentOff")}
|
|
253
|
-
isRequired
|
|
254
|
-
/>
|
|
255
|
-
) : (
|
|
256
|
-
<div className="grid grid-cols-2 gap-x-4">
|
|
257
|
-
{/* type="decimal", NOT type="currency": the currency variant hardcodes
|
|
258
|
-
a Euro symbol (FormInput.tsx:105-106), which contradicts the
|
|
259
|
-
currency selector sitting next to it. */}
|
|
260
|
-
<FormInput
|
|
261
|
-
form={form}
|
|
262
|
-
id="amountOff"
|
|
263
|
-
type="decimal"
|
|
264
|
-
name={t("billing.admin.coupons.fields.amountOff")}
|
|
265
|
-
placeholder={t("billing.admin.coupons.placeholders.amountOff")}
|
|
266
|
-
isRequired
|
|
267
|
-
/>
|
|
268
|
-
<FormSelect
|
|
269
|
-
form={form}
|
|
270
|
-
id="currency"
|
|
271
|
-
name={t("billing.admin.coupons.fields.currency")}
|
|
272
|
-
values={couponCurrencyOptions}
|
|
273
|
-
isRequired
|
|
274
|
-
/>
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
372
|
+
<FormSelect
|
|
373
|
+
form={form}
|
|
374
|
+
id="duration"
|
|
375
|
+
name={t("billing.admin.coupons.fields.duration")}
|
|
376
|
+
values={durationOptions}
|
|
377
|
+
/>
|
|
277
378
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
379
|
+
{watchDuration === "repeating" && (
|
|
380
|
+
<FormInput
|
|
381
|
+
form={form}
|
|
382
|
+
id="durationInMonths"
|
|
383
|
+
type="number"
|
|
384
|
+
name={t("billing.admin.coupons.fields.durationInMonths")}
|
|
385
|
+
placeholder={t("billing.admin.coupons.placeholders.durationInMonths")}
|
|
386
|
+
isRequired
|
|
387
|
+
/>
|
|
388
|
+
)}
|
|
284
389
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)}
|
|
295
|
-
|
|
296
|
-
<EntityMultiSelector<StripeProductInterface>
|
|
297
|
-
id="products"
|
|
298
|
-
form={form}
|
|
299
|
-
label={t("billing.admin.coupons.fields.products")}
|
|
300
|
-
description={t("billing.admin.coupons.help.products")}
|
|
301
|
-
retriever={(params) => StripeProductService.listProducts(params)}
|
|
302
|
-
module={Modules.StripeProduct}
|
|
303
|
-
getLabel={(product) => product.name}
|
|
304
|
-
/>
|
|
390
|
+
<EntityMultiSelector<StripeProductInterface>
|
|
391
|
+
id="products"
|
|
392
|
+
form={form}
|
|
393
|
+
label={t("billing.admin.coupons.fields.products")}
|
|
394
|
+
description={t("billing.admin.coupons.help.products")}
|
|
395
|
+
retriever={(params) => StripeProductService.listProducts(params)}
|
|
396
|
+
module={Modules.StripeProduct}
|
|
397
|
+
getLabel={(product) => product.name}
|
|
398
|
+
/>
|
|
305
399
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
400
|
+
<FormDateTime
|
|
401
|
+
form={form}
|
|
402
|
+
id="redeemBy"
|
|
403
|
+
name={t("billing.admin.coupons.fields.redeemBy")}
|
|
404
|
+
minDate={now}
|
|
405
|
+
allowEmpty
|
|
406
|
+
/>
|
|
313
407
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
408
|
+
<FormInput
|
|
409
|
+
form={form}
|
|
410
|
+
id="maxRedemptions"
|
|
411
|
+
type="number"
|
|
412
|
+
name={t("billing.admin.coupons.fields.maxRedemptions")}
|
|
413
|
+
placeholder={t("billing.admin.coupons.placeholders.maxRedemptions")}
|
|
414
|
+
/>
|
|
321
415
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
</>
|
|
325
|
-
)}
|
|
416
|
+
<SectionHeader>{t("billing.admin.coupons.first_code")}</SectionHeader>
|
|
417
|
+
<CouponCodeFields form={form} prefix="firstCode." />
|
|
326
418
|
</div>
|
|
327
419
|
</EditorSheet>
|
|
328
420
|
);
|
|
329
421
|
}
|
|
330
422
|
|
|
331
423
|
export default function CouponEditor(props: CouponEditorProps) {
|
|
332
|
-
return <
|
|
424
|
+
return props.coupon ? <CouponRenameEditor {...props} coupon={props.coupon} /> : <CouponCreateEditor {...props} />;
|
|
333
425
|
}
|
|
@@ -4,7 +4,7 @@ import { PowerIcon, RotateCcwIcon } from "lucide-react";
|
|
|
4
4
|
import { useTranslations } from "next-intl";
|
|
5
5
|
import { useState } from "react";
|
|
6
6
|
import { errorToast } from "../../../../../components/errors";
|
|
7
|
-
import { Button, ConfirmDialog, Switch } from "../../../../../shadcnui";
|
|
7
|
+
import { Button, ConfirmDialog, FieldLabel, Switch } from "../../../../../shadcnui";
|
|
8
8
|
import { useCouponContext } from "../../../contexts/CouponContext";
|
|
9
9
|
import { StripeCouponCodeInterface } from "../../data/stripe-coupon-code.interface";
|
|
10
10
|
import type { CouponCodeStatus } from "../../hooks/useStripeCouponCodeTableStructure";
|
|
@@ -33,7 +33,8 @@ export function CouponCodeActions({ code, status }: CouponCodeActionsProps) {
|
|
|
33
33
|
|
|
34
34
|
return (
|
|
35
35
|
<div className="flex items-center justify-end gap-x-4">
|
|
36
|
-
|
|
36
|
+
{/* Typography role 9 (field label): FieldLabel wraps the switch, so a click on the text toggles it. */}
|
|
37
|
+
<FieldLabel>
|
|
37
38
|
<Switch
|
|
38
39
|
checked={code.automatic}
|
|
39
40
|
// Switching off is always allowed; switching on only for a live code.
|
|
@@ -53,7 +54,7 @@ export function CouponCodeActions({ code, status }: CouponCodeActionsProps) {
|
|
|
53
54
|
}}
|
|
54
55
|
/>
|
|
55
56
|
{t("billing.admin.coupons.fields.automatic")}
|
|
56
|
-
</
|
|
57
|
+
</FieldLabel>
|
|
57
58
|
<Button variant="outline" size="sm" disabled={isDead} onClick={() => setOpen(true)}>
|
|
58
59
|
{isInactive ? <RotateCcwIcon /> : <PowerIcon />}
|
|
59
60
|
{t(`billing.admin.coupons.codes.${scope}.confirm`)}
|
|
@@ -29,21 +29,25 @@ export interface StripeCouponInterface extends ApiDataInterface {
|
|
|
29
29
|
// Stripe Coupon Input DTOs
|
|
30
30
|
// ============================================================================
|
|
31
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
|
+
|
|
32
41
|
export type StripeCouponInput = {
|
|
33
42
|
id: string;
|
|
34
43
|
name: string;
|
|
35
44
|
stripeCouponId?: string;
|
|
36
|
-
discountType: StripeCouponDiscountType;
|
|
37
|
-
percentOff?: number;
|
|
38
|
-
amountOff?: number;
|
|
39
|
-
currency?: string;
|
|
40
|
-
duration: StripeCouponDurationType;
|
|
41
|
-
durationInMonths?: number;
|
|
42
45
|
productIds?: string[]; // our StripeProduct ids
|
|
43
46
|
redeemBy?: Date;
|
|
44
47
|
maxRedemptions?: number;
|
|
45
48
|
firstCode: Omit<StripeCouponCodeInput, "id" | "couponId">;
|
|
46
|
-
}
|
|
49
|
+
} & StripeCouponDiscountInput &
|
|
50
|
+
StripeCouponDurationInput;
|
|
47
51
|
|
|
48
52
|
export type StripeCouponRenameInput = {
|
|
49
53
|
id: string;
|