Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cod-client-astro/locales/ar/delivery.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"active_orders": "Ψ§Ω„Ψ·Ω„Ψ¨Ψ§Ψͺ Ψ§Ω„Ω†Ψ΄Ψ·Ψ©",
"total_delivered": "Ψ₯Ψ¬Ω…Ψ§Ω„ΩŠ Ψ§Ω„ΨͺΩˆΨ΅ΩŠΩ„Ψ§Ψͺ",
"pending_cash": "Ψ§Ω„Ω†Ω‚Ψ― Ψ§Ω„Ω…ΨΉΩ„Ω‚",
"cash_drift_warning": "ΨΉΨ―Ω… ΨͺΨ·Ψ§Ψ¨Ω‚ في Ψ΅Ω†Ψ―ΩˆΩ‚ Ψ§Ω„Ψ³Ψ§Ψ¦Ω‚: Ψ§Ω„Ω…ΨΉΩ„Ω‚ {pending} Ω„ΩƒΩ† Ω…Ψ¬Ω…ΩˆΨΉ Ψ§Ω„Ψ·Ω„Ψ¨Ψ§Ψͺ غير Ψ§Ω„Ω…Ψ³ΩˆΨ§Ψ© {orders}. الفرق: {drift}. Ψ±Ψ§Ψ¬ΨΉ Ψ§Ω„ΨͺسوياΨͺ Ω‚Ψ¨Ω„ الدفع.",
"pending_cash_hint": "Ω…Ψ¨Ω„ΨΊ Ψ§Ω„ΨͺΨ­Ψ΅ΩŠΩ„ Ω„Ω… ΩŠΩΨ³Ω„ΩŽΩ‘Ω… Ψ¨ΨΉΨ―",
"total_paid": "Ψ₯Ψ¬Ω…Ψ§Ω„ΩŠ Ψ§Ω„Ω…ΩΨ³Ω„ΩŽΩ‘Ω…",
"total_paid_hint": "Ψ₯Ψ¬Ω…Ψ§Ω„ΩŠ Ψ§Ω„Ω†Ω‚Ψ― Ψ§Ω„Ω…ΩΨ³Ω„ΩŽΩ‘Ω… Ω„Ω„Ω…ΨͺΨ¬Ψ±",
Expand Down
1 change: 1 addition & 0 deletions cod-client-astro/locales/en/delivery.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"active_orders": "Active Orders",
"total_delivered": "Total Delivered",
"pending_cash": "Pending Cash",
"cash_drift_warning": "Driver cash ledger mismatch: held {pending} but unsettled orders total {orders}. Difference: {drift}. Review settlements before paying out.",
"pending_cash_hint": "COD collected, not yet remitted",
"total_paid": "Total Remitted",
"total_paid_hint": "Total cash handed to shop",
Expand Down
1 change: 1 addition & 0 deletions cod-client-astro/locales/fr/delivery.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"active_orders": "Commandes actives",
"total_delivered": "Total livrΓ©",
"pending_cash": "Espèces en attente",
"cash_drift_warning": "Écart de caisse livreur : {pending} détenus mais {orders} de commandes non réglées. Différence : {drift}. Vérifiez les règlements avant tout paiement.",
"pending_cash_hint": "COD collectΓ©, pas encore remis",
"total_paid": "Total reversΓ©",
"total_paid_hint": "Total des espèces remises à la boutique",
Expand Down
26 changes: 26 additions & 0 deletions cod-client-astro/src/features/delivery/components/DriverDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,32 @@ export function DriverDetail({ driverId }: { driverId: string }) {
</Alert>
)}
<DriverProfileHeader driver={driver} />
{driver.cashReconciliation && driver.cashReconciliation.drift !== 0 && (
<Alert role="alert" tone="warning">
<AlertCircle size={18} className="shrink-0" />
<span className="flex-1">
{t("driver_card.cash_drift_warning")
.replace(
"{pending}",
formatDeliveryMoney(driver.cashReconciliation.pendingCash, locale),
)
.replace(
"{orders}",
formatDeliveryMoney(
driver.cashReconciliation.pendingOrdersTotal,
locale,
),
)
.replace(
"{drift}",
formatDeliveryMoney(
Math.abs(driver.cashReconciliation.drift),
locale,
),
)}
</span>
</Alert>
)}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
<StatCard
label={t("driver_card.active_orders")}
Expand Down
8 changes: 8 additions & 0 deletions cod-client-astro/src/features/delivery/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ export type DriverStatus = "available" | "busy" | "inactive";
export type VehicleType = "motorcycle" | "car" | "van";
export type DriverPaymentType = "cod_remittance" | "fee_payment" | "net_settlement";

export interface DriverCashReconciliation {
pendingCash: number;
pendingOrdersTotal: number;
pendingOrdersCount: number;
drift: number;
}

export interface Driver {
id: string;
firstName: string;
Expand All @@ -19,6 +26,7 @@ export interface Driver {
totalPaid: number;
notes?: string | null;
recentOrders?: DriverOrder[];
cashReconciliation?: DriverCashReconciliation;
createdAt: string;
updatedAt: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,20 @@ beforeAll(async () => {
registry.push(mf);
rawD1 = await mf.getD1Database("DB");
const dir = resolve(__dirname, "../../db/migrations");
const preparedStatements: D1PreparedStatement[] = [];
for (const file of readdirSync(dir).filter((f) => f.endsWith(".sql")).sort()) {
const statements = readFileSync(`${dir}/${file}`, "utf8")
.split("--> statement-breakpoint")
.flatMap((s) => s.split(/;\s*\n/))
.map((s) => s.replace(/;+\s*$/, "").trim())
.filter((s) => s.replace(/--[^\n]*/g, "").trim().length > 0);
for (const statement of statements) {
await rawD1.prepare(statement).run();
preparedStatements.push(rawD1.prepare(statement));
}
}
for (let i = 0; i < preparedStatements.length; i += 50) {
await rawD1.batch(preparedStatements.slice(i, i + 50));
}
db = drizzle(rawD1 as unknown as D1Database, { schema }) as unknown as AppDb;

const now = new Date().toISOString();
Expand Down
72 changes: 39 additions & 33 deletions cod-server/src/endpoints/driver-payments/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ERROR_CODES } from "../../../../cod-shared/errors/codes";
export { getDriverPayments, getPendingSettlementOrders } from "../../../../cod-shared/queries/driver-payments";

type Database = ReturnType<typeof getDb>;
type BatchStatement = Parameters<Database["batch"]>[0][number];

/**
* Create a driver payment record and settle the selected orders.
Expand Down Expand Up @@ -82,46 +83,51 @@ export async function createDriverPayment(
const id = crypto.randomUUID();
const now = new Date().toISOString();

// Insert payment record
await db.insert(driverPayments).values({
id,
driverId,
type,
amount,
orderCount: selectedOrders.length,
notes: notes ?? null,
createdBy,
createdByName,
createdAt: now,
});
// One atomic batch: payment row + order linking + driver counters commit
// together or not at all. The previous separate awaits left drift windows β€”
// a failure after linking marked orders settled while pendingCash stayed
// inflated (phantom cash, unfixable via retry because of the settled guard).
const statements: BatchStatement[] = [
db.insert(driverPayments).values({
id,
driverId,
type,
amount,
orderCount: selectedOrders.length,
notes: notes ?? null,
createdBy,
createdByName,
createdAt: now,
}),
];

// Link orders to payment (COD settlement)
if (type === "cod_remittance" || type === "net_settlement") {
await db
.update(orders)
.set({ codPaymentId: id })
.where(inArray(orders.id, orderIds));
statements.push(
db
.update(orders)
.set({ codPaymentId: id })
.where(inArray(orders.id, orderIds)),
db
.update(drivers)
.set({
pendingCash: sql`MAX(0, ${drivers.pendingCash} - ${codTotal})`,
totalPaid: sql`${drivers.totalPaid} + ${codTotal}`,
updatedAt: now,
})
.where(eq(drivers.id, driverId)),
);
}

// Link orders to payment (fee settlement)
if (type === "fee_payment" || type === "net_settlement") {
await db
.update(orders)
.set({ feePaymentId: id })
.where(inArray(orders.id, orderIds));
statements.push(
db
.update(orders)
.set({ feePaymentId: id })
.where(inArray(orders.id, orderIds)),
);
}

// Update driver aggregate counters for COD
if (type === "cod_remittance" || type === "net_settlement") {
await db
.update(drivers)
.set({
pendingCash: sql`${drivers.pendingCash} - ${codTotal}`,
totalPaid: sql`${drivers.totalPaid} + ${codTotal}`,
updatedAt: now,
})
.where(eq(drivers.id, driverId));
}
await db.batch(statements as [BatchStatement, ...BatchStatement[]]);

return { id, driverId, type, amount, orderCount: selectedOrders.length, notes: notes ?? null, createdBy, createdByName, createdAt: now };
}
9 changes: 7 additions & 2 deletions cod-server/src/endpoints/drivers/drivers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,16 +177,21 @@ describe("updateDriverStatus", () => {
});

it("succeeds for an existing driver", async () => {
// getDriverById(1): drivers.get + compStats.get + orders.all
// getDriverById(1): drivers.get + compStats.get + [orders.all +
// reconciliation: pendingCash.get + pending aggregate.get]
// UPDATE drivers (run β€” no queue consumption)
// getDriverById(2): drivers.get + compStats.get + orders.all
// getDriverById(2): same five reads
const db = makeMockDb([
f(driverRow()),
f({ c: 0, totalFee: 0 }),
a([]),
f({ pending_cash: 0 }),
f({ total: 0, c: 0 }),
f(driverRow({ status: "busy" })),
f({ c: 0, totalFee: 0 }),
a([]),
f({ pending_cash: 0 }),
f({ total: 0, c: 0 }),
]);
const result = await updateDriverStatus(db, "drv_1", "busy");
expect(result).not.toBeNull();
Expand Down
3 changes: 3 additions & 0 deletions cod-server/src/endpoints/drivers/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ describe("Drivers Endpoint - Error Scenarios", () => {
createdAt: new Date("2024-01-01").toISOString(),
updatedAt: new Date("2024-01-15").toISOString(),
compensationWilayaCount: 0,
cashReconciliation: { pendingCash: 0, pendingOrdersTotal: 0, pendingOrdersCount: 0, drift: 0 },
recentOrders: [],
});

Expand Down Expand Up @@ -224,6 +225,7 @@ describe("Drivers Endpoint - Error Scenarios", () => {
createdAt: new Date("2024-01-01").toISOString(),
updatedAt: new Date("2024-01-15").toISOString(),
compensationWilayaCount: 0,
cashReconciliation: { pendingCash: 0, pendingOrdersTotal: 0, pendingOrdersCount: 0, drift: 0 },
recentOrders: [],
});

Expand Down Expand Up @@ -267,6 +269,7 @@ describe("Drivers Endpoint - Error Scenarios", () => {
createdAt: new Date("2024-01-01").toISOString(),
updatedAt: new Date("2024-01-15").toISOString(),
compensationWilayaCount: 0,
cashReconciliation: { pendingCash: 0, pendingOrdersTotal: 0, pendingOrdersCount: 0, drift: 0 },
recentOrders: [],
});

Expand Down
1 change: 1 addition & 0 deletions cod-server/src/endpoints/drivers/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function driverRow(overrides: Record<string, any> = {}) {
createdAt: NOW,
updatedAt: NOW,
compensationWilayaCount: 12,
cashReconciliation: { pendingCash: 0, pendingOrdersTotal: 0, pendingOrdersCount: 0, drift: 0 },
recentOrders: [],
...overrides,
};
Expand Down
Loading