cash shift

This commit is contained in:
2026-07-14 12:57:30 +03:30
parent f2da078c5a
commit 59e958e809
16 changed files with 627 additions and 5 deletions
@@ -0,0 +1,61 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260714120000_addCashShifts extends Migration {
override async up(): Promise<void> {
this.addSql(`
create table "cash_shifts" (
"id" char(26) not null,
"created_at" timestamptz not null default now(),
"updated_at" timestamptz not null default now(),
"deleted_at" timestamptz null,
"admin_id" char(26) not null,
"restaurant_id" char(26) not null,
"opened_at" timestamptz not null,
"closed_at" timestamptz null,
"opening_amount" numeric(10,0) not null default 0,
"expected_amount" numeric(10,0) null,
"counted_amount" numeric(10,0) null,
"difference_amount" numeric(10,0) null,
"orders" int not null default 0,
"status" text check ("status" in ('open', 'closed')) not null default 'open',
"notes" text null,
constraint "cash_shifts_pkey" primary key ("id")
);
`);
this.addSql(`create index "cash_shifts_created_at_index" on "cash_shifts" ("created_at");`);
this.addSql(`create index "cash_shifts_deleted_at_index" on "cash_shifts" ("deleted_at");`);
this.addSql(`create index "cash_shifts_admin_id_index" on "cash_shifts" ("admin_id");`);
this.addSql(`create index "cash_shifts_restaurant_id_index" on "cash_shifts" ("restaurant_id");`);
this.addSql(`create index "cash_shifts_restaurant_id_status_index" on "cash_shifts" ("restaurant_id", "status");`);
this.addSql(`
alter table "cash_shifts"
add constraint "cash_shifts_admin_id_foreign"
foreign key ("admin_id") references "admins" ("id") on update cascade;
`);
this.addSql(`
alter table "cash_shifts"
add constraint "cash_shifts_restaurant_id_foreign"
foreign key ("restaurant_id") references "restaurants" ("id") on update cascade;
`);
this.addSql(`alter table "orders" add column "cash_shift_id" char(26) null;`);
this.addSql(`create index "orders_cash_shift_id_index" on "orders" ("cash_shift_id");`);
this.addSql(`
alter table "orders"
add constraint "orders_cash_shift_id_foreign"
foreign key ("cash_shift_id") references "cash_shifts" ("id") on update cascade on delete set null;
`);
}
override async down(): Promise<void> {
this.addSql(`alter table "orders" drop constraint if exists "orders_cash_shift_id_foreign";`);
this.addSql(`drop index if exists "orders_cash_shift_id_index";`);
this.addSql(`alter table "orders" drop column if exists "cash_shift_id";`);
this.addSql(`alter table "cash_shifts" drop constraint if exists "cash_shifts_restaurant_id_foreign";`);
this.addSql(`alter table "cash_shifts" drop constraint if exists "cash_shifts_admin_id_foreign";`);
this.addSql(`drop table if exists "cash_shifts" cascade;`);
}
}
+8
View File
@@ -788,6 +788,14 @@ export const enum PagerMessage {
CREATED_SUCCESS = 'درخواست پیج با موفقیت ایجاد شد',
}
export const enum CashShiftMessage {
NOT_FOUND = 'شیفت صندوق یافت نشد',
ALREADY_OPEN = 'یک شیفت صندوق باز از قبل وجود دارد',
ALREADY_CLOSED = 'این شیفت صندوق قبلا بسته شده است',
ADMIN_NOT_FOUND = 'مدیر یافت نشد',
RESTAURANT_NOT_FOUND = 'رستوران یافت نشد',
}
export const enum UserSuccessMessage {
ADDRESS_DELETED_SUCCESS = 'آدرس با موفقیت حذف شد',
SCORE_CONVERTED_SUCCESS = 'امتیاز با موفقیت به کیف پول تبدیل شد',
@@ -0,0 +1,83 @@
import { Controller, Get, Post, Param, UseGuards, Query, Body, Patch } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiBody } from '@nestjs/swagger';
import { CashShiftsService } from '../providers/cash-shifts.service';
import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard';
import { RestId } from 'src/common/decorators/rest-id.decorator';
import { AdminId } from 'src/common/decorators/admin-id.decorator';
import { Permissions } from 'src/common/decorators/permissions.decorator';
import { Permission } from 'src/common/enums/permission.enum';
import { OpenCashShiftDto } from '../dto/open-cash-shift.dto';
import { CloseCashShiftDto } from '../dto/close-cash-shift.dto';
import { FindCashShiftsDto } from '../dto/find-cash-shifts.dto';
@ApiTags('cash-shifts')
@ApiBearerAuth()
@Controller()
export class CashShiftsController {
constructor(private readonly cashShiftsService: CashShiftsService) {}
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ORDERS)
@Post('admin/cash-shifts/open')
@ApiOperation({ summary: 'Open a new cash shift' })
@ApiBody({ type: OpenCashShiftDto })
openShift(@RestId() restId: string, @AdminId() adminId: string, @Body() dto: OpenCashShiftDto) {
return this.cashShiftsService.openShift(restId, adminId, dto);
}
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ORDERS)
@Patch('admin/cash-shifts/:shiftId/close')
@ApiOperation({ summary: 'Close an open cash shift' })
@ApiParam({ name: 'shiftId', description: 'Cash shift ID' })
@ApiBody({ type: CloseCashShiftDto })
closeShift(
@Param('shiftId') shiftId: string,
@RestId() restId: string,
@Body() dto: CloseCashShiftDto,
) {
return this.cashShiftsService.closeShift(shiftId, restId, dto);
}
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ORDERS)
@Get('admin/cash-shifts/current/summary')
@ApiOperation({ summary: 'Calculate summary for the current open cash shift' })
getCurrentShiftSummary(@RestId() restId: string) {
return this.cashShiftsService.calculateCurrentShiftSummary(restId);
}
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ORDERS)
@Get('admin/cash-shifts/current')
@ApiOperation({ summary: 'Get the current open cash shift' })
getCurrentOpenShift(@RestId() restId: string) {
return this.cashShiftsService.getCurrentOpenShift(restId);
}
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ORDERS)
@Get('admin/cash-shifts')
@ApiOperation({ summary: 'List cash shifts with pagination and filters' })
findAll(@RestId() restId: string, @Query() dto: FindCashShiftsDto) {
return this.cashShiftsService.findAll(restId, dto);
}
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ORDERS)
@Get('admin/cash-shifts/:shiftId/summary')
@ApiOperation({ summary: 'Calculate summary for a cash shift' })
@ApiParam({ name: 'shiftId', description: 'Cash shift ID' })
getShiftSummary(@Param('shiftId') shiftId: string, @RestId() restId: string) {
return this.cashShiftsService.calculateShiftSummary(shiftId, restId);
}
@UseGuards(AdminAuthGuard)
@Permissions(Permission.MANAGE_ORDERS)
@Get('admin/cash-shifts/:shiftId')
@ApiOperation({ summary: 'Get a cash shift by ID' })
@ApiParam({ name: 'shiftId', description: 'Cash shift ID' })
findOne(@Param('shiftId') shiftId: string, @RestId() restId: string) {
return this.cashShiftsService.findOne(shiftId, restId);
}
}
@@ -0,0 +1,16 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNumber, IsOptional, IsString, Min } from 'class-validator';
export class CloseCashShiftDto {
@ApiProperty({ description: 'Physically counted cash amount', minimum: 0 })
@Type(() => Number)
@IsNumber()
@Min(0)
countedAmount!: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
notes?: string;
}
@@ -0,0 +1,61 @@
import {
IsDateString,
IsEnum,
IsIn,
IsNumber,
IsOptional,
IsString,
Min,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { CashShiftStatus } from '../interface/cash-shift.interface';
const sortOrderOptions = ['asc', 'desc'] as const;
type SortOrder = (typeof sortOrderOptions)[number];
export class FindCashShiftsDto {
@ApiPropertyOptional({ default: 1, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page: number = 1;
@ApiPropertyOptional({ default: 10, minimum: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
limit: number = 10;
@ApiPropertyOptional({ enum: CashShiftStatus })
@IsOptional()
@IsEnum(CashShiftStatus)
status?: CashShiftStatus;
@ApiPropertyOptional()
@IsOptional()
@IsString()
adminId?: string;
@ApiPropertyOptional({ format: 'date-time' })
@IsOptional()
@IsDateString()
startDate?: string;
@ApiPropertyOptional({ format: 'date-time' })
@IsOptional()
@IsDateString()
endDate?: string;
@ApiPropertyOptional({ default: 'openedAt' })
@IsOptional()
@IsString()
orderBy: string = 'openedAt';
@ApiPropertyOptional({ enum: sortOrderOptions, default: 'desc' })
@IsOptional()
@IsIn(sortOrderOptions)
order: SortOrder = 'desc';
}
@@ -68,6 +68,11 @@ export class FindOrdersDto {
@IsDateString()
endDate?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
cashShiftId?: string;
@ApiPropertyOptional({ default: 'createdAt' })
@IsOptional()
@IsString()
@@ -0,0 +1,16 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNumber, IsOptional, IsString, Min } from 'class-validator';
export class OpenCashShiftDto {
@ApiProperty({ description: 'Opening cash amount in drawer', minimum: 0 })
@Type(() => Number)
@IsNumber()
@Min(0)
openingAmount!: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
notes?: string;
}
@@ -0,0 +1,48 @@
import { Collection, Entity, Enum, Index, ManyToOne, OneToMany, Property } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { Admin } from '../../admin/entities/admin.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { CashShiftStatus } from '../interface/cash-shift.interface';
import { Order } from './order.entity';
@Entity({ tableName: 'cash_shifts' })
@Index({ properties: ['restaurant'] })
@Index({ properties: ['restaurant', 'status'] })
@Index({ properties: ['admin'] })
export class CashShift extends BaseEntity {
@ManyToOne(() => Admin)
admin!: Admin;
@ManyToOne(() => Restaurant)
restaurant!: Restaurant;
@Property({ columnType: 'timestamptz' })
openedAt: Date = new Date();
@Property({ columnType: 'timestamptz', nullable: true })
closedAt?: Date | null;
@Property({ type: 'decimal', precision: 10, scale: 0, default: 0 })
openingAmount: number = 0;
@Property({ type: 'decimal', precision: 10, scale: 0, nullable: true })
expectedAmount?: number | null;
@Property({ type: 'decimal', precision: 10, scale: 0, nullable: true })
countedAmount?: number | null;
@Property({ type: 'decimal', precision: 10, scale: 0, nullable: true })
differenceAmount?: number | null;
@Property({ type: 'int', default: 0, fieldName: 'orders' })
ordersCount: number = 0;
@Enum(() => CashShiftStatus)
status: CashShiftStatus = CashShiftStatus.OPEN;
@Property({ type: 'text', nullable: true })
notes?: string | null;
@OneToMany(() => Order, order => order.cashShift)
shiftOrders = new Collection<Order>(this);
}
@@ -20,11 +20,13 @@ import { OrderItem } from './order-item.entity';
import { Delivery } from '../../delivery/entities/delivery.entity';
import { OrderUserAddress, OrderCarAddress } from '../interface/order.interface';
import { Payment } from 'src/modules/payments/entities/payment.entity';
import { CashShift } from './cash-shift.entity';
@Entity({ tableName: 'orders' })
@Index({ properties: ['restaurant'] })
@Index({ properties: ['restaurant', 'status'] })
@Index({ properties: ['restaurant', 'orderNumber'] })
@Index({ properties: ['cashShift'] })
export class Order extends BaseEntity {
@ManyToOne(() => User, { nullable: true })
user?: User | null;
@@ -32,6 +34,9 @@ export class Order extends BaseEntity {
@ManyToOne(() => Restaurant)
restaurant!: Restaurant;
@ManyToOne(() => CashShift, { nullable: true })
cashShift?: CashShift | null;
@ManyToOne(() => Delivery)
deliveryMethod!: Delivery;
@@ -0,0 +1,21 @@
import { PaymentMethodEnum } from '../../payments/interface/payment';
export enum CashShiftStatus {
OPEN = 'open',
CLOSED = 'closed',
}
export const SHIFT_REVENUE_PAYMENT_METHODS = [
PaymentMethodEnum.Cash,
PaymentMethodEnum.Online,
PaymentMethodEnum.CreditCard,
] as const;
export interface CashShiftSummary {
ordersCount: number;
cashReceived: number;
onlineReceived: number;
creditCardReceived: number;
totalReceived: number;
expectedAmount: number;
}
+8 -4
View File
@@ -1,9 +1,12 @@
import { Module, forwardRef } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { OrdersService } from './providers/orders.service';
import { CashShiftsService } from './providers/cash-shifts.service';
import { OrdersController } from './controllers/orders.controller';
import { CashShiftsController } from './controllers/cash-shifts.controller';
import { Order } from './entities/order.entity';
import { OrderItem } from './entities/order-item.entity';
import { CashShift } from './entities/cash-shift.entity';
import { User } from '../users/entities/user.entity';
import { Restaurant } from '../restaurants/entities/restaurant.entity';
import { Food } from '../foods/entities/food.entity';
@@ -15,6 +18,7 @@ import { AuthModule } from '../auth/auth.module';
import { PaymentsModule } from '../payments/payments.module';
import { JwtModule } from '@nestjs/jwt';
import { OrderRepository } from './repositories/order.repository';
import { CashShiftRepository } from './repositories/cash-shift.repository';
import { OrderListeners } from './listeners/order.listeners';
import { OrdersCrone } from './crone/order.crone';
import { AdminModule } from '../admin/admin.module';
@@ -24,7 +28,7 @@ import { UserModule } from '../users/user.module';
@Module({
imports: [
MikroOrmModule.forFeature([Order, OrderItem, User, Restaurant, Food, UserAddress, PaymentMethod]),
MikroOrmModule.forFeature([Order, OrderItem, CashShift, User, Restaurant, Food, UserAddress, PaymentMethod]),
CartModule,
UtilsModule,
AuthModule,
@@ -35,8 +39,8 @@ import { UserModule } from '../users/user.module';
InventoryModule,
forwardRef(() => UserModule)
],
controllers: [OrdersController],
providers: [OrdersService, OrderRepository, OrderListeners, OrdersCrone],
exports: [OrderRepository, OrdersService],
controllers: [OrdersController, CashShiftsController],
providers: [OrdersService, CashShiftsService, OrderRepository, CashShiftRepository, OrderListeners, OrdersCrone],
exports: [OrderRepository, OrdersService, CashShiftsService],
})
export class OrdersModule { }
@@ -0,0 +1,157 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { CashShift } from '../entities/cash-shift.entity';
import { CashShiftStatus, CashShiftSummary } from '../interface/cash-shift.interface';
import { CashShiftRepository } from '../repositories/cash-shift.repository';
import { OpenCashShiftDto } from '../dto/open-cash-shift.dto';
import { CloseCashShiftDto } from '../dto/close-cash-shift.dto';
import { FindCashShiftsDto } from '../dto/find-cash-shifts.dto';
import { Admin } from '../../admin/entities/admin.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { Order } from '../entities/order.entity';
import { CashShiftMessage } from 'src/common/enums/message.enum';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { calculateCashShiftSummary } from '../utils/calculate-cash-shift-summary.util';
@Injectable()
export class CashShiftsService {
constructor(
private readonly em: EntityManager,
private readonly cashShiftRepository: CashShiftRepository,
) {}
async openShift(restaurantId: string, adminId: string, dto: OpenCashShiftDto): Promise<CashShift> {
const existingOpenShift = await this.cashShiftRepository.findOpenShift(restaurantId);
if (existingOpenShift) {
throw new BadRequestException(CashShiftMessage.ALREADY_OPEN);
}
const [admin, restaurant] = await Promise.all([
this.em.findOne(Admin, { id: adminId }),
this.em.findOne(Restaurant, { id: restaurantId }),
]);
if (!admin) {
throw new NotFoundException(CashShiftMessage.ADMIN_NOT_FOUND);
}
if (!restaurant) {
throw new NotFoundException(CashShiftMessage.RESTAURANT_NOT_FOUND);
}
const shift = this.em.create(CashShift, {
admin,
restaurant,
openedAt: new Date(),
openingAmount: dto.openingAmount,
ordersCount: 0,
notes: dto.notes ?? null,
status: CashShiftStatus.OPEN,
});
await this.em.persistAndFlush(shift);
await this.em.populate(shift, ['admin', 'restaurant']);
return shift;
}
async closeShift(shiftId: string, restaurantId: string, dto: CloseCashShiftDto): Promise<CashShift> {
const shift = await this.em.findOne(
CashShift,
{ id: shiftId, restaurant: { id: restaurantId } },
{ populate: ['admin', 'restaurant'] },
);
if (!shift) {
throw new NotFoundException(CashShiftMessage.NOT_FOUND);
}
if (shift.status === CashShiftStatus.CLOSED) {
throw new BadRequestException(CashShiftMessage.ALREADY_CLOSED);
}
const summary = await this.calculateShiftSummary(shiftId, restaurantId);
const countedAmount = dto.countedAmount;
shift.expectedAmount = summary.expectedAmount;
shift.countedAmount = countedAmount;
shift.differenceAmount = countedAmount - summary.expectedAmount;
shift.ordersCount = summary.ordersCount;
shift.closedAt = new Date();
shift.status = CashShiftStatus.CLOSED;
if (dto.notes !== undefined) {
shift.notes = dto.notes;
}
await this.em.persistAndFlush(shift);
return shift;
}
async calculateShiftSummary(shiftId: string, restaurantId: string): Promise<CashShiftSummary> {
const shift = await this.em.findOne(CashShift, { id: shiftId, restaurant: { id: restaurantId } });
if (!shift) {
throw new NotFoundException(CashShiftMessage.NOT_FOUND);
}
const orders = await this.getShiftOrders(shift, restaurantId);
return calculateCashShiftSummary(Number(shift.openingAmount), orders);
}
async calculateCurrentShiftSummary(restaurantId: string): Promise<CashShiftSummary | null> {
const shift = await this.cashShiftRepository.findOpenShift(restaurantId);
if (!shift) {
return null;
}
const orders = await this.getShiftOrders(shift, restaurantId);
return calculateCashShiftSummary(Number(shift.openingAmount), orders);
}
findAll(restaurantId: string, dto: FindCashShiftsDto): Promise<PaginatedResult<CashShift>> {
return this.cashShiftRepository.findAllPaginated(restaurantId, dto);
}
async findOne(shiftId: string, restaurantId: string): Promise<CashShift> {
const shift = await this.em.findOne(
CashShift,
{ id: shiftId, restaurant: { id: restaurantId } },
{ populate: ['admin', 'restaurant'] },
);
if (!shift) {
throw new NotFoundException(CashShiftMessage.NOT_FOUND);
}
return shift;
}
getCurrentOpenShift(restaurantId: string): Promise<CashShift | null> {
return this.cashShiftRepository.findOpenShift(restaurantId);
}
async assignOrderToOpenShift(order: Order, em: EntityManager = this.em): Promise<void> {
if (order.cashShift) {
return;
}
const restaurantId = typeof order.restaurant === 'string' ? order.restaurant : order.restaurant.id;
const openShift = await em.findOne(CashShift, {
restaurant: { id: restaurantId },
status: CashShiftStatus.OPEN,
});
if (!openShift) {
return;
}
order.cashShift = openShift;
openShift.ordersCount += 1;
em.persist(order);
em.persist(openShift);
}
private getShiftOrders(shift: CashShift, restaurantId: string): Promise<Order[]> {
return this.em.find(
Order,
{ cashShift: shift, restaurant: { id: restaurantId } },
{ populate: ['payments'] },
);
}
}
@@ -29,6 +29,7 @@ import { AdminCreateOrderDto } from '../dto/admin-create-order.dto';
import { AdminUpdateOrderFeesDto } from '../dto/admin-update-order-fees.dto';
import { FoodOrderReportDto, FoodOrderReportSortBy } from '../dto/food-order-report.dto';
import { DailyOrderReportDto, DailyOrderReportSortBy } from '../dto/daily-order-report.dto';
import { CashShiftsService } from './cash-shifts.service';
type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number };
type ValidatedCartForOrder = {
@@ -63,6 +64,7 @@ export class OrdersService {
private readonly paymentsService: PaymentsService,
private readonly inventoryService: InventoryService,
private readonly eventEmitter: EventEmitter2,
private readonly cashShiftsService: CashShiftsService,
) { }
async checkout(userId: string, restaurantId: string) {
@@ -133,6 +135,7 @@ export class OrdersService {
})),
};
await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto);
await this.cashShiftsService.assignOrderToOpenShift(order, em);
await em.flush();
this.logger.debug(`Order ${order.id} created for user ${userId} (restaurant ${restaurantId})`);
return order;
@@ -230,6 +233,7 @@ export class OrdersService {
items: orderItemsData.map(item => ({ foodId: item.food.id, quantity: item.quantity })),
};
await this.inventoryService.deductFromInventory(em, bulkReserveFoodDto);
await this.cashShiftsService.assignOrderToOpenShift(order, em);
await em.flush();
this.logger.debug(`Admin created order ${order.id} for user ${user?.id || 'anonymous'} (restaurant: ${restaurantId})`);
@@ -347,6 +351,7 @@ export class OrdersService {
orderBy: dto.orderBy,
order: dto.order,
excludeOnlinePendingPayment: true,
cashShiftId: dto.cashShiftId,
});
return result;
@@ -363,6 +368,7 @@ export class OrdersService {
'userAddress',
'carAddress',
'paymentMethod',
'cashShift',
'payments',
'items',
'items.food',
@@ -0,0 +1,82 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { FilterQuery } from '@mikro-orm/core';
import { CashShift } from '../entities/cash-shift.entity';
import { CashShiftStatus } from '../interface/cash-shift.interface';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
type FindCashShiftsOpts = {
page?: number;
limit?: number;
status?: CashShiftStatus;
adminId?: string;
startDate?: string;
endDate?: string;
orderBy?: string;
order?: 'asc' | 'desc';
};
@Injectable()
export class CashShiftRepository extends EntityRepository<CashShift> {
constructor(readonly em: EntityManager) {
super(em, CashShift);
}
async findAllPaginated(restId: string, opts: FindCashShiftsOpts = {}): Promise<PaginatedResult<CashShift>> {
const {
page = 1,
limit = 10,
status,
adminId,
startDate,
endDate,
orderBy = 'openedAt',
order = 'desc',
} = opts;
const offset = (page - 1) * limit;
const where: FilterQuery<CashShift> = { restaurant: { id: restId } };
if (status) {
where.status = status;
}
if (adminId) {
where.admin = { id: adminId };
}
if (startDate || endDate) {
where.openedAt = {};
if (startDate) {
where.openedAt.$gte = new Date(startDate);
}
if (endDate) {
where.openedAt.$lte = new Date(endDate);
}
}
const [data, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order },
populate: ['admin', 'restaurant'],
});
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
findOpenShift(restId: string): Promise<CashShift | null> {
return this.findOne(
{ restaurant: { id: restId }, status: CashShiftStatus.OPEN },
{ populate: ['admin'] },
);
}
}
@@ -18,6 +18,7 @@ type FindOrdersOpts = {
orderBy?: string;
order?: 'asc' | 'desc';
userId?: string;
cashShiftId?: string;
excludeOnlinePendingPayment?: boolean;
};
@@ -43,6 +44,7 @@ export class OrderRepository extends EntityRepository<Order> {
order = 'desc',
userId,
excludeOnlinePendingPayment = false,
cashShiftId,
} = opts;
const offset = (page - 1) * limit;
@@ -70,6 +72,10 @@ export class OrderRepository extends EntityRepository<Order> {
where.user = { id: userId };
}
if (cashShiftId) {
where.cashShift = { id: cashShiftId };
}
// Filter by date range
if (startDate || endDate) {
where.createdAt = {};
@@ -118,7 +124,7 @@ export class OrderRepository extends EntityRepository<Order> {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
populate: ['user', 'restaurant', 'deliveryMethod', 'paymentMethod', 'items', 'items.food'] as never,
populate: ['user', 'restaurant', 'deliveryMethod', 'paymentMethod', 'cashShift', 'items', 'items.food'] as never,
});
// Collect all (orderId, foodId) pairs for efficient review lookup
@@ -0,0 +1,43 @@
import { Order } from '../entities/order.entity';
import { PaymentMethodEnum, PaymentStatusEnum } from '../../payments/interface/payment';
import { CashShiftSummary, SHIFT_REVENUE_PAYMENT_METHODS } from '../interface/cash-shift.interface';
const shiftRevenueMethods = new Set<PaymentMethodEnum>(SHIFT_REVENUE_PAYMENT_METHODS);
export function calculateCashShiftSummary(openingAmount: number, orders: Order[]): CashShiftSummary {
let cashReceived = 0;
let onlineReceived = 0;
let creditCardReceived = 0;
for (const order of orders) {
for (const payment of order.payments.getItems()) {
if (payment.status !== PaymentStatusEnum.Paid || !shiftRevenueMethods.has(payment.method)) {
continue;
}
const amount = Number(payment.amount);
switch (payment.method) {
case PaymentMethodEnum.Cash:
cashReceived += amount;
break;
case PaymentMethodEnum.Online:
onlineReceived += amount;
break;
case PaymentMethodEnum.CreditCard:
creditCardReceived += amount;
break;
}
}
}
const totalReceived = cashReceived + onlineReceived + creditCardReceived;
return {
ordersCount: orders.length,
cashReceived,
onlineReceived,
creditCardReceived,
totalReceived,
expectedAmount: Number(openingAmount) + totalReceived,
};
}