Implement order lifecycle logic for Optika Zoom — state machine transitions, split payments, refunds, discount approval flow. Use when working on orders, order status changes, checkout, payment processing, refund logic, or discount approval.
new → in_work → [waiting_lens] → ready → delivered
↘ cancelled (только admin/superadmin)
const ALLOWED_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {
new: ['in_work', 'cancelled'],
in_work: ['waiting_lens', 'ready', 'cancelled'],
waiting_lens: ['in_work', 'ready'],
ready: ['delivered'],
delivered: [],
cancelled: [],
};
delivered — блокировка-- Триггер: запрет delivered если недоплата
CREATE OR REPLACE FUNCTION check_order_payment() RETURNS trigger AS $$
DECLARE
paid numeric;
BEGIN
IF NEW.status = 'delivered' THEN
SELECT COALESCE(SUM(amount), 0) INTO paid
FROM payments WHERE order_id = NEW.id AND status = 'confirmed';
IF paid < NEW.total_amount AND NEW.credit_approved = false THEN
RAISE EXCEPTION 'Order % is not fully paid (paid: %, total: %)',
NEW.id, paid, NEW.total_amount;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Всегда через RPC, никогда не через цепочку .insert() вызовов:
const { data, error } = await supabase.rpc('create_order', {
p_patient_id: patientId,
p_branch_id: branchId,
p_items: orderItems, // [{ sku_id, quantity, price, is_refundable }]
p_prescription_id: rxId,
p_payment: firstPayment, // { method, amount }
p_discount_percent: discount,
p_discount_reason: reason,
});
// Хук для проверки нужен ли approve
function useDiscountApproval(percent: number) {
const { role } = useAuthStore();
const needsApproval = percent > 10 && role === 'seller';
return { needsApproval };
}
Пока discount_approval_status = 'pending' — заказ остаётся в new, кнопка "Передать в мастерскую" заблокирована.
// Разрешённые к возврату item_types
const REFUNDABLE_TYPES = ['frame', 'consumable'] as const;
async function processRefund(orderId: string, itemIds: string[]) {
// 1. Проверить is_refundable = true для каждого item
// 2. RPC: вернуть товар на склад + создать refund транзакцию
const { error } = await supabase.rpc('process_refund', {
p_order_id: orderId,
p_item_ids: itemIds,
});
}
// Компонент PaymentSplitter
// Отображать остаток к оплате: totalAmount - SUM(confirmedPayments)
// Добавлять платёжные строки до тех пор пока остаток > 0
// Кнопка "Выдать" активна только когда остаток === 0 || credit_approved