Rust discount function (one automatic discount evaluating all offers in priority order, parity with the Odoo engine), React Router admin with Polaris, theme app extension and app proxy pages, webhooks incl. GDPR, managed-pricing limits, and a Docker Compose stack (Postgres + Caddy HTTPS) for a single VPS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
58 lines
2.6 KiB
TypeScript
58 lines
2.6 KiB
TypeScript
// Offer validation (F22). Same rules as the Odoo constraints; used by the editor (inline) and the server.
|
|
import { num } from "./offer-text";
|
|
|
|
export interface OfferInput {
|
|
name: string;
|
|
badge?: string | null;
|
|
description?: string | null;
|
|
type: string;
|
|
discountType: string;
|
|
discountValue: number | string;
|
|
mixQty: number;
|
|
buyQty: number;
|
|
getQty: number;
|
|
getPercent: number | string;
|
|
startsAt?: string | null;
|
|
endsAt?: string | null;
|
|
maxPerOrder: number;
|
|
sequence: number;
|
|
lines: { productId: string; variantId?: string | null; quantity: number }[];
|
|
scopes: { role: string; kind: string; refId: string }[];
|
|
}
|
|
|
|
export type Errors = Partial<Record<string, string>>;
|
|
|
|
export function validateOffer(o: OfferInput): Errors {
|
|
const e: Errors = {};
|
|
if (!o.name?.trim()) e.name = "Name is required.";
|
|
else if (o.name.length > 80) e.name = "Use at most 80 characters (it is shown at checkout).";
|
|
if (!["fixed", "mix", "bxgy"].includes(o.type)) e.type = "Choose a bundle type.";
|
|
if (o.startsAt && o.endsAt && new Date(o.endsAt) < new Date(o.startsAt)) {
|
|
e.endsAt = "The end date must be after the start date.";
|
|
}
|
|
if (!(o.maxPerOrder >= 0) || !Number.isInteger(o.maxPerOrder)) e.maxPerOrder = "Use 0 (no limit) or a whole number.";
|
|
if (o.type === "fixed" || o.type === "mix") {
|
|
if (!["percent", "amount", "fixed_price"].includes(o.discountType)) e.discountType = "Choose a discount type.";
|
|
const v = num(o.discountValue);
|
|
if (!(v > 0)) e.discountValue = "The discount value must be greater than 0.";
|
|
else if (o.discountType === "percent" && v > 100) e.discountValue = "A percentage can't be more than 100.";
|
|
}
|
|
if (o.type === "fixed") {
|
|
if (!o.lines.length) e.lines = "Add at least one product to the bundle.";
|
|
else if (o.lines.some((l) => !(l.quantity >= 1) || !Number.isInteger(l.quantity))) e.lines = "Each quantity must be at least 1.";
|
|
}
|
|
if (o.type === "mix") {
|
|
if (!o.scopes.some((s) => s.role === "mix")) e.scopes_mix = "Add products or collections.";
|
|
if (!(o.mixQty >= 1)) e.mixQty = "The number of items must be at least 1.";
|
|
}
|
|
if (o.type === "bxgy") {
|
|
if (!o.scopes.some((s) => s.role === "buy")) e.scopes_buy = "Add what the customer buys.";
|
|
if (!o.scopes.some((s) => s.role === "get")) e.scopes_get = "Add what the customer gets.";
|
|
if (!(o.buyQty >= 1)) e.buyQty = "The buy quantity must be at least 1.";
|
|
if (!(o.getQty >= 1)) e.getQty = "The get quantity must be at least 1.";
|
|
const gp = num(o.getPercent);
|
|
if (!(gp > 0 && gp <= 100)) e.getPercent = "Use a percentage above 0 and up to 100 (100 = free).";
|
|
}
|
|
return e;
|
|
}
|