@carlonicora/nextjs-jsonapi 5.6.0 → 5.7.1
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-AE3ZTEEH.js → BlockNoteEditor-ARUXU5AV.js} +19 -19
- package/dist/{BlockNoteEditor-AE3ZTEEH.js.map → BlockNoteEditor-ARUXU5AV.js.map} +1 -1
- package/dist/{BlockNoteEditor-NEYG5L6W.mjs → BlockNoteEditor-MNNISNVN.mjs} +4 -4
- 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 +591 -574
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +30 -13
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-SFYPXCK4.mjs → chunk-2FNSDBJG.mjs} +641 -536
- package/dist/chunk-2FNSDBJG.mjs.map +1 -0
- 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-LQ4DK4JF.js → chunk-2WK35GO2.js} +1317 -1212
- package/dist/chunk-2WK35GO2.js.map +1 -0
- package/dist/{chunk-HKZAESS6.js → chunk-5TH2DSAH.js} +17 -12
- package/dist/chunk-5TH2DSAH.js.map +1 -0
- 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/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/containers/CouponContainer.tsx +2 -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 +294 -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-coupon/utils/__tests__/coupon-page-module.spec.ts +32 -0
- package/src/features/billing/stripe-coupon/utils/coupon-page-module.ts +13 -0
- 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/chunk-SFYPXCK4.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-NEYG5L6W.mjs.map → BlockNoteEditor-MNNISNVN.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 { couponPageModule } from "../../utils/coupon-page-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,105 @@ 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
|
+
function CouponRenameEditor({
|
|
49
155
|
coupon,
|
|
50
156
|
propagateChanges,
|
|
51
157
|
onSuccess,
|
|
@@ -54,85 +160,116 @@ function CouponEditorInternal({
|
|
|
54
160
|
onClose,
|
|
55
161
|
dialogOpen,
|
|
56
162
|
onDialogOpenChange,
|
|
57
|
-
}: CouponEditorProps) {
|
|
163
|
+
}: CouponEditorProps & { coupon: StripeCouponInterface }) {
|
|
58
164
|
const t = useTranslations();
|
|
59
165
|
const fmt = useBillingFormatOptions();
|
|
60
166
|
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
|
-
|
|
167
|
+
const { renameCoupon } = useCouponContext();
|
|
168
|
+
|
|
169
|
+
const formSchema = useMemo(() => couponRenameSchema(t), [t]);
|
|
170
|
+
|
|
171
|
+
const getDefaultValues = useCallback((): CouponRenameValues => ({ id: coupon.id, name: coupon.name }), [coupon]);
|
|
172
|
+
|
|
173
|
+
const form = useForm<CouponRenameValues>({
|
|
174
|
+
resolver: zodResolver(formSchema),
|
|
175
|
+
defaultValues: getDefaultValues(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<EditorSheet
|
|
180
|
+
form={form}
|
|
181
|
+
entityType={t("billing.admin.coupons.entity")}
|
|
182
|
+
entityName={coupon.name}
|
|
183
|
+
isEdit
|
|
184
|
+
module={couponPageModule()}
|
|
185
|
+
size="lg"
|
|
186
|
+
propagateChanges={propagateChanges}
|
|
187
|
+
onSuccess={onSuccess}
|
|
188
|
+
onNavigate={(url) => router.push(url)}
|
|
189
|
+
onSubmit={async (values) => await renameCoupon({ id: coupon.id, name: values.name })}
|
|
190
|
+
onReset={getDefaultValues}
|
|
191
|
+
trigger={trigger}
|
|
192
|
+
forceShow={forceShow}
|
|
193
|
+
onClose={onClose}
|
|
194
|
+
dialogOpen={dialogOpen}
|
|
195
|
+
onDialogOpenChange={onDialogOpenChange}
|
|
196
|
+
>
|
|
197
|
+
<div className="flex w-full flex-col gap-y-4">
|
|
198
|
+
<FormInput
|
|
199
|
+
form={form}
|
|
200
|
+
id="name"
|
|
201
|
+
name={t("billing.admin.coupons.fields.name")}
|
|
202
|
+
placeholder={t("billing.admin.coupons.placeholders.name")}
|
|
203
|
+
isRequired
|
|
204
|
+
/>
|
|
205
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
206
|
+
<DetailField
|
|
207
|
+
label={t("billing.admin.coupons.fields.discount")}
|
|
208
|
+
value={<span className="tabular-nums">{formatCouponDiscount(coupon, t, fmt)}</span>}
|
|
209
|
+
/>
|
|
210
|
+
<DetailField
|
|
211
|
+
label={t("billing.admin.coupons.fields.redeemBy")}
|
|
212
|
+
value={<span className="tabular-nums">{coupon.redeemBy ? formatDate(coupon.redeemBy, fmt) : "—"}</span>}
|
|
213
|
+
/>
|
|
214
|
+
<DetailField
|
|
215
|
+
label={t("billing.admin.coupons.fields.maxRedemptions")}
|
|
216
|
+
value={
|
|
217
|
+
<span className="tabular-nums">
|
|
218
|
+
{coupon.maxRedemptions ?? t("billing.admin.coupons.places.no_limit")}
|
|
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
|
+
</div>
|
|
230
|
+
</EditorSheet>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function CouponCreateEditor({
|
|
235
|
+
propagateChanges,
|
|
236
|
+
onSuccess,
|
|
237
|
+
trigger,
|
|
238
|
+
forceShow,
|
|
239
|
+
onClose,
|
|
240
|
+
dialogOpen,
|
|
241
|
+
onDialogOpenChange,
|
|
242
|
+
}: Omit<CouponEditorProps, "coupon">) {
|
|
243
|
+
const t = useTranslations();
|
|
244
|
+
const router = useI18nRouter();
|
|
245
|
+
const { createCoupon } = useCouponContext();
|
|
246
|
+
// Read once on mount: render stays pure.
|
|
247
|
+
const [now] = useState(() => new Date());
|
|
248
|
+
|
|
249
|
+
const formSchema = useMemo(() => couponCreateSchema(t), [t]);
|
|
110
250
|
|
|
111
251
|
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],
|
|
252
|
+
(): CouponCreateValues => ({
|
|
253
|
+
id: v4(),
|
|
254
|
+
name: "",
|
|
255
|
+
stripeCouponId: "",
|
|
256
|
+
discountType: "percent_off",
|
|
257
|
+
percentOff: "",
|
|
258
|
+
amountOff: "",
|
|
259
|
+
currency: "eur",
|
|
260
|
+
duration: "forever",
|
|
261
|
+
// "" not undefined for every FormInput: see couponCodeDefaults.
|
|
262
|
+
durationInMonths: "",
|
|
263
|
+
products: [],
|
|
264
|
+
redeemBy: undefined,
|
|
265
|
+
maxRedemptions: "",
|
|
266
|
+
firstCode: couponCodeDefaults(),
|
|
267
|
+
}),
|
|
268
|
+
[],
|
|
132
269
|
);
|
|
133
270
|
|
|
134
|
-
const form = useForm<
|
|
135
|
-
resolver: zodResolver(formSchema
|
|
271
|
+
const form = useForm<CouponCreateValues>({
|
|
272
|
+
resolver: zodResolver(formSchema),
|
|
136
273
|
defaultValues: getDefaultValues(),
|
|
137
274
|
});
|
|
138
275
|
|
|
@@ -154,36 +291,13 @@ function CouponEditorInternal({
|
|
|
154
291
|
<EditorSheet
|
|
155
292
|
form={form}
|
|
156
293
|
entityType={t("billing.admin.coupons.entity")}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
module={Modules.StripeCoupon}
|
|
294
|
+
isEdit={false}
|
|
295
|
+
module={couponPageModule()}
|
|
160
296
|
size="lg"
|
|
161
297
|
propagateChanges={propagateChanges}
|
|
162
298
|
onSuccess={onSuccess}
|
|
163
299
|
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
|
-
}}
|
|
300
|
+
onSubmit={async (values) => await createCoupon(toCouponInput(values))}
|
|
187
301
|
onReset={getDefaultValues}
|
|
188
302
|
trigger={trigger}
|
|
189
303
|
forceShow={forceShow}
|
|
@@ -199,135 +313,103 @@ function CouponEditorInternal({
|
|
|
199
313
|
placeholder={t("billing.admin.coupons.placeholders.name")}
|
|
200
314
|
isRequired
|
|
201
315
|
/>
|
|
316
|
+
<FormInput
|
|
317
|
+
form={form}
|
|
318
|
+
id="stripeCouponId"
|
|
319
|
+
name={t("billing.admin.coupons.fields.stripeCouponId")}
|
|
320
|
+
placeholder={t("billing.admin.coupons.placeholders.stripeCouponId")}
|
|
321
|
+
/>
|
|
202
322
|
|
|
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
|
-
</>
|
|
323
|
+
<FormSelect
|
|
324
|
+
form={form}
|
|
325
|
+
id="discountType"
|
|
326
|
+
name={t("billing.admin.coupons.fields.discountType")}
|
|
327
|
+
values={discountTypeOptions}
|
|
328
|
+
/>
|
|
329
|
+
|
|
330
|
+
{watchDiscountType === "percent_off" ? (
|
|
331
|
+
<FormInput
|
|
332
|
+
form={form}
|
|
333
|
+
id="percentOff"
|
|
334
|
+
type="decimal"
|
|
335
|
+
name={t("billing.admin.coupons.fields.percentOff")}
|
|
336
|
+
placeholder={t("billing.admin.coupons.placeholders.percentOff")}
|
|
337
|
+
isRequired
|
|
338
|
+
/>
|
|
230
339
|
) : (
|
|
231
|
-
|
|
340
|
+
<div className="grid grid-cols-2 gap-x-4">
|
|
341
|
+
{/* type="decimal", NOT type="currency": the currency variant hardcodes
|
|
342
|
+
a Euro symbol (FormInput.tsx:105-106), which contradicts the
|
|
343
|
+
currency selector sitting next to it. */}
|
|
232
344
|
<FormInput
|
|
233
345
|
form={form}
|
|
234
|
-
id="
|
|
235
|
-
|
|
236
|
-
|
|
346
|
+
id="amountOff"
|
|
347
|
+
type="decimal"
|
|
348
|
+
name={t("billing.admin.coupons.fields.amountOff")}
|
|
349
|
+
placeholder={t("billing.admin.coupons.placeholders.amountOff")}
|
|
350
|
+
isRequired
|
|
237
351
|
/>
|
|
238
|
-
|
|
239
352
|
<FormSelect
|
|
240
353
|
form={form}
|
|
241
|
-
id="
|
|
242
|
-
name={t("billing.admin.coupons.fields.
|
|
243
|
-
values={
|
|
354
|
+
id="currency"
|
|
355
|
+
name={t("billing.admin.coupons.fields.currency")}
|
|
356
|
+
values={couponCurrencyOptions}
|
|
357
|
+
isRequired
|
|
244
358
|
/>
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
245
361
|
|
|
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
|
-
)}
|
|
362
|
+
<FormSelect
|
|
363
|
+
form={form}
|
|
364
|
+
id="duration"
|
|
365
|
+
name={t("billing.admin.coupons.fields.duration")}
|
|
366
|
+
values={durationOptions}
|
|
367
|
+
/>
|
|
277
368
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
369
|
+
{watchDuration === "repeating" && (
|
|
370
|
+
<FormInput
|
|
371
|
+
form={form}
|
|
372
|
+
id="durationInMonths"
|
|
373
|
+
type="number"
|
|
374
|
+
name={t("billing.admin.coupons.fields.durationInMonths")}
|
|
375
|
+
placeholder={t("billing.admin.coupons.placeholders.durationInMonths")}
|
|
376
|
+
isRequired
|
|
377
|
+
/>
|
|
378
|
+
)}
|
|
284
379
|
|
|
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
|
-
/>
|
|
380
|
+
<EntityMultiSelector<StripeProductInterface>
|
|
381
|
+
id="products"
|
|
382
|
+
form={form}
|
|
383
|
+
label={t("billing.admin.coupons.fields.products")}
|
|
384
|
+
description={t("billing.admin.coupons.help.products")}
|
|
385
|
+
retriever={(params) => StripeProductService.listProducts(params)}
|
|
386
|
+
module={Modules.StripeProduct}
|
|
387
|
+
getLabel={(product) => product.name}
|
|
388
|
+
/>
|
|
305
389
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
390
|
+
<FormDateTime
|
|
391
|
+
form={form}
|
|
392
|
+
id="redeemBy"
|
|
393
|
+
name={t("billing.admin.coupons.fields.redeemBy")}
|
|
394
|
+
minDate={now}
|
|
395
|
+
allowEmpty
|
|
396
|
+
/>
|
|
313
397
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
398
|
+
<FormInput
|
|
399
|
+
form={form}
|
|
400
|
+
id="maxRedemptions"
|
|
401
|
+
type="number"
|
|
402
|
+
name={t("billing.admin.coupons.fields.maxRedemptions")}
|
|
403
|
+
placeholder={t("billing.admin.coupons.placeholders.maxRedemptions")}
|
|
404
|
+
/>
|
|
321
405
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
</>
|
|
325
|
-
)}
|
|
406
|
+
<SectionHeader>{t("billing.admin.coupons.first_code")}</SectionHeader>
|
|
407
|
+
<CouponCodeFields form={form} prefix="firstCode." />
|
|
326
408
|
</div>
|
|
327
409
|
</EditorSheet>
|
|
328
410
|
);
|
|
329
411
|
}
|
|
330
412
|
|
|
331
413
|
export default function CouponEditor(props: CouponEditorProps) {
|
|
332
|
-
return <
|
|
414
|
+
return props.coupon ? <CouponRenameEditor {...props} coupon={props.coupon} /> : <CouponCreateEditor {...props} />;
|
|
333
415
|
}
|
|
@@ -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;
|
|
@@ -124,10 +124,12 @@ export class StripeCoupon extends AbstractApiData implements StripeCouponInterfa
|
|
|
124
124
|
|
|
125
125
|
const attributes = response.data.attributes;
|
|
126
126
|
if (data.stripeCouponId) attributes.stripeCouponId = data.stripeCouponId;
|
|
127
|
-
if (data.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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;
|
|
131
133
|
if (data.productIds && data.productIds.length > 0) attributes.productIds = data.productIds;
|
|
132
134
|
if (data.redeemBy !== undefined) attributes.redeemBy = data.redeemBy.toISOString();
|
|
133
135
|
if (data.maxRedemptions !== undefined) attributes.maxRedemptions = data.maxRedemptions;
|