feat: product design surveys, cart expiry/buy-later, and catalog polish
Add like/dislike surveys with majority alerts for manufacturing decisions, clearer accounting mapping copy, and related cart/catalog admin-app improvements. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { CartExpiryWorkerService } from './cart-expiry.worker';
|
||||
import { tehranWallToUtc } from './cart-schedule';
|
||||
|
||||
describe('CartExpiryWorkerService', () => {
|
||||
function build(opts: {
|
||||
cfg?: Record<string, unknown>;
|
||||
carts?: Array<Record<string, unknown>>;
|
||||
}) {
|
||||
const carts = opts.carts ?? [];
|
||||
const updates: Array<{ customerId: string; data: Record<string, unknown> }> = [];
|
||||
const sms = { send: jest.fn(async () => ({ ok: true })) };
|
||||
const prisma = {
|
||||
customerCart: {
|
||||
findMany: jest.fn(async () => carts),
|
||||
update: jest.fn(async ({ where, data }: { where: { customerId: string }; data: Record<string, unknown> }) => {
|
||||
updates.push({ customerId: where.customerId, data });
|
||||
return {};
|
||||
}),
|
||||
},
|
||||
$executeRaw: jest.fn(async () => 1),
|
||||
};
|
||||
const cart = {
|
||||
getConfig: jest.fn(async () => ({
|
||||
enabled: true,
|
||||
clearHour: 13,
|
||||
clearMinute: 0,
|
||||
sameDayDeferFromHour: 8,
|
||||
sameDayDeferFromMinute: 0,
|
||||
reminderEnabled: true,
|
||||
reminderHoursBefore: 2,
|
||||
reminderBody: 'تا %count ساعت',
|
||||
worker: 'on' as const,
|
||||
...opts.cfg,
|
||||
})),
|
||||
};
|
||||
const worker = new CartExpiryWorkerService(prisma as never, cart as never, sms as never);
|
||||
return { worker, sms, updates, prisma, cart };
|
||||
}
|
||||
|
||||
it('no-ops when disabled', async () => {
|
||||
const { worker, sms, prisma } = build({ cfg: { enabled: false } });
|
||||
await worker.tick(tehranWallToUtc(2026, 11, 12, 12, 0, 0));
|
||||
expect(sms.send).not.toHaveBeenCalled();
|
||||
expect(prisma.customerCart.findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reminds carts due today (touched before 08:00), not deferred ones', async () => {
|
||||
const todayClear = tehranWallToUtc(2026, 11, 12, 13, 0, 0);
|
||||
const { worker, sms, updates } = build({
|
||||
carts: [
|
||||
{
|
||||
customerId: 'early',
|
||||
items: [{ pid: 'p1', vid: 'v1', qty: 1 }],
|
||||
reminderSentForClearAt: null,
|
||||
updatedAt: tehranWallToUtc(2026, 11, 12, 7, 0, 0),
|
||||
customer: { mobile: '09120000000', isActive: true },
|
||||
},
|
||||
{
|
||||
customerId: 'deferred',
|
||||
items: [{ pid: 'p2', vid: 'v2', qty: 1 }],
|
||||
reminderSentForClearAt: null,
|
||||
// 10:00 → deadline tomorrow 13 — no reminder yet at 11:30 today
|
||||
updatedAt: tehranWallToUtc(2026, 11, 12, 10, 0, 0),
|
||||
customer: { mobile: '09121111111', isActive: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
await worker.tick(tehranWallToUtc(2026, 11, 12, 11, 30, 0));
|
||||
expect(sms.send).toHaveBeenCalledTimes(1);
|
||||
expect(sms.send).toHaveBeenCalledWith('09120000000', expect.any(String));
|
||||
expect(updates[0]?.customerId).toBe('early');
|
||||
expect(updates[0]?.data.reminderSentForClearAt).toEqual(todayClear);
|
||||
});
|
||||
|
||||
it('does not clear 08–13 carts at today 13; clears pre-08 carts', async () => {
|
||||
const { worker, updates, sms } = build({
|
||||
carts: [
|
||||
{
|
||||
customerId: 'early',
|
||||
items: [{ pid: 'p1', vid: 'v1', qty: 1 }],
|
||||
reminderSentForClearAt: null,
|
||||
updatedAt: tehranWallToUtc(2026, 11, 12, 7, 0, 0),
|
||||
customer: { mobile: '09120000000', isActive: true },
|
||||
},
|
||||
{
|
||||
customerId: 'deferred',
|
||||
items: [{ pid: 'p2', vid: 'v2', qty: 1 }],
|
||||
reminderSentForClearAt: null,
|
||||
updatedAt: tehranWallToUtc(2026, 11, 12, 10, 0, 0),
|
||||
customer: { mobile: '09121111111', isActive: true },
|
||||
},
|
||||
{
|
||||
customerId: 'afternoon',
|
||||
items: [{ pid: 'p3', vid: 'v3', qty: 1 }],
|
||||
reminderSentForClearAt: null,
|
||||
updatedAt: tehranWallToUtc(2026, 11, 12, 14, 0, 0),
|
||||
customer: { mobile: '09122222222', isActive: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
await worker.tick(tehranWallToUtc(2026, 11, 12, 13, 5, 0));
|
||||
expect(sms.send).not.toHaveBeenCalled();
|
||||
expect(updates).toHaveLength(1);
|
||||
expect(updates[0].customerId).toBe('early');
|
||||
expect(updates[0].data.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('clears deferred (08–13) carts at tomorrow 13', async () => {
|
||||
const { worker, updates } = build({
|
||||
carts: [
|
||||
{
|
||||
customerId: 'deferred',
|
||||
items: [{ pid: 'p2', vid: 'v2', qty: 1 }],
|
||||
reminderSentForClearAt: null,
|
||||
updatedAt: tehranWallToUtc(2026, 11, 12, 10, 0, 0),
|
||||
customer: { mobile: '09121111111', isActive: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
await worker.tick(tehranWallToUtc(2026, 11, 13, 13, 5, 0));
|
||||
expect(updates).toHaveLength(1);
|
||||
expect(updates[0].customerId).toBe('deferred');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user