This commit is contained in:
@@ -91,10 +91,7 @@ export class VenueTabs {
|
||||
}
|
||||
|
||||
case 'tab_deleted':
|
||||
if (this.tabs.delete(tabId)) {
|
||||
this.changed.delete(tabId);
|
||||
this.deleted.add(tabId);
|
||||
}
|
||||
this.remove(tabId);
|
||||
return null;
|
||||
|
||||
case 'line_added': {
|
||||
@@ -195,6 +192,14 @@ export class VenueTabs {
|
||||
};
|
||||
}
|
||||
|
||||
/** Deletes a tab, from a terminal or the dashboard. False when it is not open. */
|
||||
remove(tabId: string): boolean {
|
||||
if (!this.tabs.delete(tabId)) return false;
|
||||
this.changed.delete(tabId);
|
||||
this.deleted.add(tabId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Opens a tab without an event: planned customers, and taking over a terminal's tabs. */
|
||||
open(tabId: string, customerName: string, openedAt: Date): TabState {
|
||||
const tab: TabState = { id: tabId, customerName, openedAt, version: 0, lines: [] };
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { Controller, Delete, Get, Param, ParseIntPipe, Query, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { TabsService } from './tabs.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { VenueAccessGuard } from '../common/guards/venue-access.guard';
|
||||
import { RolesGuard } from '../common/guards/roles.guard';
|
||||
import { Roles } from '../common/decorators/roles.decorator';
|
||||
|
||||
@ApiTags('Tabs')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, VenueAccessGuard)
|
||||
@UseGuards(JwtAuthGuard, VenueAccessGuard, RolesGuard)
|
||||
@Controller('venues/:venueId/tabs')
|
||||
export class TabsController {
|
||||
constructor(private tabsService: TabsService) {}
|
||||
@@ -16,4 +18,17 @@ export class TabsController {
|
||||
async findAll(@Param('venueId') venueId: string) {
|
||||
return this.tabsService.findAll(venueId);
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':tabId')
|
||||
@Roles('admin', 'manager')
|
||||
@ApiOperation({ summary: 'Delete an open tab without settling it' })
|
||||
@ApiQuery({ name: 'expectedVersion', required: false, description: 'Refuse when the tab changed since this version' })
|
||||
async remove(
|
||||
@Param('venueId') venueId: string,
|
||||
@Param('tabId') tabId: string,
|
||||
@Req() req: any,
|
||||
@Query('expectedVersion', new ParseIntPipe({ optional: true })) expectedVersion?: number,
|
||||
) {
|
||||
return this.tabsService.remove(venueId, tabId, expectedVersion, req.user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { RealtimeService } from '../realtime/realtime.service';
|
||||
import { customerNameKey } from '../planned-tabs/customer-name';
|
||||
import { tabSummary } from './tab-state';
|
||||
import { VenueLedger } from './venue-ledger';
|
||||
|
||||
@Injectable()
|
||||
export class TabsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private realtime: RealtimeService,
|
||||
) {}
|
||||
|
||||
/** The venue's open tabs as the server holds them; every terminal shows the same. */
|
||||
async findAll(venueId: string) {
|
||||
@@ -30,4 +36,46 @@ export class TabsService {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a tab without settling it, as tab_deleted does on a terminal: what is still on it is not paid and stays
|
||||
* out of the stock. The terminals drop it on their next sync; one that still has unsent drinks for it brings it
|
||||
* back, so no sale is lost. [expectedVersion] refuses a tab that changed since it was shown.
|
||||
*/
|
||||
async remove(venueId: string, tabId: string, expectedVersion: number | undefined, user: { id: string }) {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const now = new Date();
|
||||
const ledger = await VenueLedger.open(tx, venueId);
|
||||
|
||||
const tab = ledger.tabs.get(tabId);
|
||||
if (!tab) {
|
||||
throw new NotFoundException({ code: 'tab_not_found', message: 'This tab is no longer open' });
|
||||
}
|
||||
if (expectedVersion !== undefined && tab.version !== expectedVersion) {
|
||||
throw new ConflictException({
|
||||
code: 'version_mismatch',
|
||||
message: 'The tab has changed since it was loaded. Refresh and try again.',
|
||||
});
|
||||
}
|
||||
|
||||
const { customerName, totalInCents, items } = tabSummary({ ...tab, updatedAt: now });
|
||||
ledger.tabs.remove(tabId);
|
||||
await ledger.save(tx, now);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
venueId,
|
||||
actorType: 'client_user',
|
||||
actorId: user.id,
|
||||
action: 'tab_deleted',
|
||||
details: JSON.stringify({ tabId, customerName, amountInCents: totalInCents, items }),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
this.realtime.notify(venueId, 'tabs');
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user