Address the 11 October test report findings
The initial test report (run before the pilot-readiness changes) raised seven findings and a retest plan. This closes the ones still open. - F-01: a business on its free trial may run 2 active branches whatever its tier, so an owner can try transfers and per-branch stock. Only active branches count towards any limit. - F-06: owners can rename branches and deactivate/reactivate them from the Branches page. A branch can only be deactivated once it holds no stock and no active staff; its history stays in every report, and it leaves every picker. - F-04: the Products page lists deactivated products under a toggle with Reactivate. Deactivating warns about stock still on hand, which stays on the Stock page so it can be transferred or counted out. - F-05: products carry a unit (L, ml, kg, g, pcs, tray, pack) instead of everything being litres. - CAT-12: product prices must be above zero and names are unique per business, ignoring case. - F-03: a reversed report period is refused by the page and the API, and the page labels which period the cards show. Report dates are Nairobi business days. - F-07: cost of goods sold is units sold x average buying price. Stock bought in the period is reported separately, with a cash surplus line, so unsold stock no longer shows as a loss. - STA-04: staff and new branch managers can be invited with a one-time, 7-day link; the person sets their own password. Managers can issue a new link for their own cashiers. - OFF-02 / POS-02: the till shows each product's stock and deducts every sale immediately, online or offline; it warns before selling more than the recorded stock, and has a Clear sale button. - npm run backfill:stock turns purchases recorded before the stock ledger into stock, so existing tenants (and the report's ZERRO fixtures) start from what they bought. tests/retest-plan.test.ts runs the report's completion and retest plan and checks every balance and total it predicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,10 @@ import { z } from "zod";
|
||||
import { isValidObjectId } from "mongoose";
|
||||
import { connectDb } from "@/lib/server/db";
|
||||
import { Branch } from "@/lib/server/models/Branch";
|
||||
import { User } from "@/lib/server/models/User";
|
||||
import { authorize } from "@/lib/server/auth";
|
||||
import { branchLimitError } from "@/lib/server/subscription";
|
||||
import { getStockLevels } from "@/lib/server/stock";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
@@ -11,10 +14,21 @@ const updateSchema = z
|
||||
.object({
|
||||
branchName: z.string().trim().min(1).max(120).optional(),
|
||||
locationCity: z.string().trim().min(1).max(120).optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
})
|
||||
.refine((v) => Object.keys(v).length > 0, { message: "No fields to update" });
|
||||
|
||||
// PATCH /api/branches/:id — owner renames a branch or changes its city.
|
||||
/**
|
||||
* PATCH /api/branches/:id — owner renames a branch, or deactivates /
|
||||
* reactivates it. This is how an owner "removes" a branch: nothing is
|
||||
* deleted, so its sales, purchases and stock history stay in every report.
|
||||
*
|
||||
* A branch can only be deactivated once it is empty, so nothing is left
|
||||
* stranded in it:
|
||||
* - no stock on hand (transfer it out or count it to zero first), and
|
||||
* - no active staff (move or deactivate them first).
|
||||
* Reactivating counts against the plan's branch limit again.
|
||||
*/
|
||||
export async function PATCH(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
await connectDb();
|
||||
const gate = authorize(req, ["SUPER_ADMIN"]);
|
||||
@@ -28,10 +42,36 @@ export async function PATCH(req: NextRequest, { params }: { params: { id: string
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid request" }, { status: 400 });
|
||||
}
|
||||
const { isActive, ...details } = parsed.data;
|
||||
|
||||
const branch = await Branch.findOneAndUpdate({ _id: params.id, businessId: auth.businessId }, parsed.data, {
|
||||
new: true,
|
||||
}).lean();
|
||||
const branch = await Branch.findOne({ _id: params.id, businessId: auth.businessId });
|
||||
if (!branch) return NextResponse.json({ error: "Branch not found" }, { status: 404 });
|
||||
const wasActive = branch.isActive !== false;
|
||||
|
||||
if (isActive === false && wasActive) {
|
||||
const stock = (await getStockLevels(auth.businessId, branch._id)).filter((l) => l.quantity !== 0);
|
||||
if (stock.length > 0) {
|
||||
const list = stock.map((l) => `${l.quantity} x ${l.productLabel}`).join(", ");
|
||||
return NextResponse.json(
|
||||
{ error: `Move or count out this branch's stock first (${list}).`, stock },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
const staff = await User.countDocuments({ businessId: auth.businessId, branchId: branch._id, isActive: true });
|
||||
if (staff > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: `${staff} active staff member(s) still work at this branch. Move or deactivate them first.` },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isActive === true && !wasActive) {
|
||||
const limitError = await branchLimitError(auth.businessId);
|
||||
if (limitError) return NextResponse.json({ error: limitError }, { status: 403 });
|
||||
}
|
||||
|
||||
Object.assign(branch, details);
|
||||
if (isActive !== undefined) branch.isActive = isActive;
|
||||
await branch.save();
|
||||
return NextResponse.json({ branch });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user