feat: promotion usage limit (#13760)
* feat: promotion usage limit * fix: update, refactor tests, parallel case * fix: batch update, cleanup unused map * feat: paralel campaign and promotion tests * chore: changesets, fix i18 schema * fix: ui tweaks * chore: refactor --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
@@ -7683,6 +7683,12 @@
|
||||
"taxInclusive": {
|
||||
"type": "string"
|
||||
},
|
||||
"usageLimit": {
|
||||
"type": "string"
|
||||
},
|
||||
"usage": {
|
||||
"type": "string"
|
||||
},
|
||||
"amount": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7784,6 +7790,8 @@
|
||||
"addCondition",
|
||||
"clearAll",
|
||||
"taxInclusive",
|
||||
"usageLimit",
|
||||
"usage",
|
||||
"amount",
|
||||
"conditions"
|
||||
],
|
||||
@@ -8230,6 +8238,19 @@
|
||||
},
|
||||
"required": ["fixed", "percentage"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "description"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8245,7 +8266,8 @@
|
||||
"allocation",
|
||||
"code",
|
||||
"value",
|
||||
"value_type"
|
||||
"value_type",
|
||||
"limit"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
@@ -2051,6 +2051,8 @@
|
||||
"addCondition": "Add condition",
|
||||
"clearAll": "Clear all",
|
||||
"taxInclusive": "Tax Inclusive",
|
||||
"usageLimit": "Usage Limit",
|
||||
"usage": "Usage",
|
||||
"amount": {
|
||||
"tooltip": "Select the currency code to enable setting the amount"
|
||||
},
|
||||
@@ -2212,6 +2214,10 @@
|
||||
"title": "Percentage",
|
||||
"description": "The percentage to discount off the amount. eg. 8%"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"title": "Usage Limit",
|
||||
"description": "Maximum number of times this promotion can be used across all orders. Leave empty for unlimited usage."
|
||||
}
|
||||
},
|
||||
"deleteWarning": "You are about to delete the promotion {{code}}. This action cannot be undone.",
|
||||
|
||||
@@ -58,6 +58,7 @@ const defaultValues = {
|
||||
status: "draft" as PromotionStatusValues,
|
||||
rules: [],
|
||||
is_tax_inclusive: false,
|
||||
limit: undefined,
|
||||
application_method: {
|
||||
allocation: "each" as ApplicationMethodAllocationValues,
|
||||
type: "fixed" as ApplicationMethodTypeValues,
|
||||
@@ -901,7 +902,9 @@ export const CreatePromotionForm = () => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label
|
||||
tooltip={t("promotions.fields.allocationTooltip")}
|
||||
tooltip={t(
|
||||
"promotions.fields.allocationTooltip"
|
||||
)}
|
||||
>
|
||||
{t("promotions.fields.allocation")}
|
||||
</Form.Label>
|
||||
@@ -987,6 +990,42 @@ export const CreatePromotionForm = () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="limit"
|
||||
render={({ field: { onChange, value, ...field } }) => {
|
||||
return (
|
||||
<Form.Item className="basis-1/2">
|
||||
<Form.Label>
|
||||
{t("promotions.form.limit.title")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={1}
|
||||
value={value ?? ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
onChange(val === "" ? null : parseInt(val, 10))
|
||||
}}
|
||||
placeholder="100"
|
||||
/>
|
||||
</Form.Control>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="text-ui-fg-subtle"
|
||||
>
|
||||
{t("promotions.form.limit.description")}
|
||||
</Text>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ProgressTabs.Content>
|
||||
|
||||
@@ -28,6 +28,7 @@ export const CreatePromotionSchema = z
|
||||
status: z.enum(["draft", "active", "inactive"]),
|
||||
rules: RuleSchema,
|
||||
is_tax_inclusive: z.boolean().optional(),
|
||||
limit: z.number().int().min(1).nullable().optional(),
|
||||
application_method: z.object({
|
||||
allocation: z.enum(["each", "across", "once"]),
|
||||
value: z.number().min(0).or(z.string().min(1)),
|
||||
|
||||
@@ -196,6 +196,20 @@ export const PromotionGeneralSection = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{typeof promotion.limit === "number" && (
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
Usage Limit
|
||||
</Text>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Text className="inline" size="small" leading="compact">
|
||||
{promotion.used || 0} / {promotion.limit}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user