feat: move to V2
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-10-29 11:21:52 +01:00
parent 6342b8451e
commit 314717ba3a
244 changed files with 11615 additions and 15292 deletions
+28 -93
View File
@@ -1,40 +1,9 @@
import { Prisma } from '@prisma/client';
import { ensureDefaultCustomers } from '../planned-tabs/default-customers';
type Tx = Prisma.TransactionClient;
/**
* What a superadmin can wipe from a venue. Users, terminals and the product catalog always stay: a reset is for
* starting a new event or season with the same bar.
*/
export const RESET_SCOPES = [
// The open tabs. Terminals drop them on their next sync; drinks a terminal still has to send bring a tab back
'tabs',
// Paid tabs: the sales history
'settlements',
// What the terminals reported. Also the record of which events were applied, so a terminal that re-sends an old
// event would apply it again: only wipe these when every terminal has synced
'tabEvents',
// Planned customers and the reminders sent to them
'plannedTabs',
// Every tracked product's stock is set to one count
'stock',
// Stock deliveries, breakage and counts made on the dashboard
'stockHistory',
// Command history
'commands',
'auditLog',
] as const;
export type ResetScope = (typeof RESET_SCOPES)[number];
import { ResetScope } from '@kooltap/shared';
import { Tx } from '../prisma/prisma.service';
import { ensureDefaultCustomers } from '../customers/default-customers';
export type ResetCounts = Partial<Record<ResetScope, number>>;
/** Locks the venue like a sync does, so no terminal changes it halfway through. */
export async function lockVenue(tx: Tx, venueId: string) {
await tx.$queryRaw`SELECT "id" FROM "Venue" WHERE "id" = ${venueId} FOR UPDATE`;
}
/** How many rows each scope would remove (or, for stock, how many products it would set). */
export async function countReset(tx: Tx, venueId: string, scopes: ResetScope[]): Promise<ResetCounts> {
const where = { venueId };
@@ -42,10 +11,9 @@ export async function countReset(tx: Tx, venueId: string, scopes: ResetScope[]):
tabs: () => tx.tab.count({ where }),
settlements: () => tx.settlement.count({ where }),
tabEvents: () => tx.tabEvent.count({ where }),
plannedTabs: () => tx.plannedTab.count({ where }),
customers: () => tx.customer.count({ where }),
stock: () => tx.product.count({ where: { venueId, stock: { not: null } } }),
stockHistory: () => tx.stockAdjustment.count({ where }),
commands: () => tx.remoteCommand.count({ where }),
stockHistory: () => tx.stockMovement.count({ where }),
auditLog: () => tx.auditLog.count({ where }),
};
const counts: ResetCounts = {};
@@ -54,14 +22,21 @@ export async function countReset(tx: Tx, venueId: string, scopes: ResetScope[]):
}
/**
* Wipes [scopes] of a venue inside [tx], which must have locked it. [stockValue] is the count every tracked product
* gets; it is recorded as an applied stock count, after the stock history is wiped, so the history explains it.
* Wipes [scopes] of a venue inside [tx], which must have locked it. Users, terminals and the catalog always stay: a
* reset is for starting a new event or season with the same bar.
*
* - tabs: terminals drop them on their next sync; drinks a terminal still has to send bring a tab back
* - tabEvents: also the record of which events were applied, so a terminal that sends an old event again would
* apply it again: only wipe these when every terminal has synced
* - customers: and the reminders sent to them
* - stock: every tracked product gets [stockValue] as a count, recorded after the stock history is wiped so the
* history explains it
*/
export async function resetVenue(
tx: Tx,
venueId: string,
scopes: ResetScope[],
options: { stockValue: number; actor: { id: string; name: string }; now: Date },
options: { stockValue: number; userId: string; now: Date },
): Promise<ResetCounts> {
const where = { venueId };
const counts: ResetCounts = {};
@@ -70,65 +45,25 @@ export async function resetVenue(
if (has('tabs')) counts.tabs = (await tx.tab.deleteMany({ where })).count;
if (has('settlements')) counts.settlements = (await tx.settlement.deleteMany({ where })).count;
if (has('tabEvents')) counts.tabEvents = (await tx.tabEvent.deleteMany({ where })).count;
if (has('plannedTabs')) {
await tx.tabReminder.deleteMany({ where });
counts.plannedTabs = (await tx.plannedTab.deleteMany({ where })).count;
if (has('customers')) {
await tx.reminder.deleteMany({ where });
counts.customers = (await tx.customer.deleteMany({ where })).count;
}
// The Free Drinks tab is always there: planned again, and opened on the next sync
if (has('tabs') || has('plannedTabs')) await ensureDefaultCustomers(tx, venueId);
if (has('stockHistory')) counts.stockHistory = (await tx.stockAdjustment.deleteMany({ where })).count;
// The Free Drinks tab is always there: added again, and opened on the next sync
if (has('tabs') || has('customers')) await ensureDefaultCustomers(tx, venueId);
if (has('stockHistory')) counts.stockHistory = (await tx.stockMovement.deleteMany({ where })).count;
if (has('stock')) {
const { now, actor, stockValue } = options;
const tracked = await tx.product.findMany({
where: { venueId, stock: { not: null } },
select: { productId: true, name: true },
const { now, userId, stockValue } = options;
const tracked = await tx.product.findMany({ where: { venueId, stock: { not: null } }, select: { id: true } });
await tx.product.updateMany({ where: { venueId, stock: { not: null } }, data: { stock: stockValue, countedAt: now } });
await tx.stockMovement.createMany({
data: tracked.map((product) => ({
venueId, productId: product.id, type: 'count' as const, quantity: stockValue, stockAfter: stockValue, note: 'Stock reset', userId, createdAt: now,
})),
});
await tx.product.updateMany({
where: { venueId, stock: { not: null } },
data: { stock: stockValue, countedAt: now },
});
if (tracked.length > 0) {
await tx.stockAdjustment.createMany({
data: tracked.map((product) => ({
venueId,
productId: product.productId,
productName: product.name,
type: 'set',
quantity: stockValue,
note: 'Stock reset',
requestedBy: actor.name,
requestedById: actor.id,
status: 'applied',
stockAfter: stockValue,
createdAt: now,
processedAt: now,
})),
});
}
counts.stock = tracked.length;
}
if (has('commands')) counts.commands = (await tx.remoteCommand.deleteMany({ where })).count;
if (has('auditLog')) counts.auditLog = (await tx.auditLog.deleteMany({ where })).count;
return counts;
}
/** Deletes a venue and everything in it, users and terminals included. */
export async function deleteVenue(tx: Tx, venueId: string) {
const where = { venueId };
const users = await tx.clientUser.findMany({ where, select: { id: true } });
await tx.auditLog.deleteMany({ where });
await tx.tabEvent.deleteMany({ where });
await tx.tabReminder.deleteMany({ where });
await tx.plannedTab.deleteMany({ where });
await tx.remoteCommand.deleteMany({ where });
await tx.settlement.deleteMany({ where });
await tx.tab.deleteMany({ where });
await tx.stockAdjustment.deleteMany({ where });
await tx.product.deleteMany({ where });
await tx.registrationCode.deleteMany({ where });
await tx.device.deleteMany({ where });
await tx.refreshToken.deleteMany({ where: { userId: { in: users.map((u) => u.id) } } });
await tx.clientUser.deleteMany({ where });
await tx.venue.delete({ where: { id: venueId } });
}