Branches: - 5 services: branch, schedule, inventory-settings, payment-terminal, user-assignment - Hierarchical management, schedules, terminals Products: - 6 services: category, product, price, supplier, attribute, variant - Hierarchical categories, multi-pricing, variants Projects: - 6 services: project, task, timesheet, milestone, member, stage - Kanban support, timesheet approval workflow Sales: - 2 services: quotation, sales-order - Full sales workflow with quotation-to-order conversion Invoices: - 2 services: invoice, payment - Complete invoicing with payment allocation Notifications: - 5 services: notification, preference, template, channel, in-app - Multi-channel support (email, sms, push, whatsapp, in-app) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
241 lines
6.5 KiB
TypeScript
241 lines
6.5 KiB
TypeScript
/**
|
|
* BranchPaymentTerminal Service
|
|
* Servicio para gestión de terminales de pago por sucursal
|
|
*
|
|
* @module Branches
|
|
*/
|
|
|
|
import { Repository, FindOptionsWhere } from 'typeorm';
|
|
import { AppDataSource } from '../../../shared/database/typeorm.config';
|
|
import {
|
|
BranchPaymentTerminal,
|
|
TerminalProvider,
|
|
HealthStatus,
|
|
} from '../entities/branch-payment-terminal.entity';
|
|
import { ServiceContext } from './branch.service';
|
|
|
|
export interface CreateBranchPaymentTerminalDto {
|
|
branchId: string;
|
|
terminalId: string;
|
|
terminalName?: string;
|
|
terminalProvider: TerminalProvider;
|
|
provider: string;
|
|
credentials?: Record<string, any>;
|
|
isActive?: boolean;
|
|
isPrimary?: boolean;
|
|
dailyLimit?: number;
|
|
transactionLimit?: number;
|
|
config?: Record<string, any>;
|
|
}
|
|
|
|
export interface UpdateBranchPaymentTerminalDto {
|
|
terminalName?: string;
|
|
terminalProvider?: TerminalProvider;
|
|
provider?: string;
|
|
credentials?: Record<string, any>;
|
|
isActive?: boolean;
|
|
isPrimary?: boolean;
|
|
healthStatus?: HealthStatus;
|
|
dailyLimit?: number;
|
|
transactionLimit?: number;
|
|
config?: Record<string, any>;
|
|
}
|
|
|
|
export interface BranchPaymentTerminalFilters {
|
|
branchId?: string;
|
|
terminalProvider?: TerminalProvider;
|
|
isActive?: boolean;
|
|
healthStatus?: HealthStatus;
|
|
}
|
|
|
|
export class BranchPaymentTerminalService {
|
|
private repository: Repository<BranchPaymentTerminal>;
|
|
|
|
constructor() {
|
|
this.repository = AppDataSource.getRepository(BranchPaymentTerminal);
|
|
}
|
|
|
|
async findAll(
|
|
ctx: ServiceContext,
|
|
filters?: BranchPaymentTerminalFilters
|
|
): Promise<BranchPaymentTerminal[]> {
|
|
const where: FindOptionsWhere<BranchPaymentTerminal> = {
|
|
tenantId: ctx.tenantId,
|
|
};
|
|
|
|
if (filters?.branchId) {
|
|
where.branchId = filters.branchId;
|
|
}
|
|
|
|
if (filters?.terminalProvider) {
|
|
where.terminalProvider = filters.terminalProvider;
|
|
}
|
|
|
|
if (filters?.isActive !== undefined) {
|
|
where.isActive = filters.isActive;
|
|
}
|
|
|
|
if (filters?.healthStatus) {
|
|
where.healthStatus = filters.healthStatus;
|
|
}
|
|
|
|
return this.repository.find({
|
|
where,
|
|
order: { isPrimary: 'DESC', terminalName: 'ASC' },
|
|
});
|
|
}
|
|
|
|
async findById(ctx: ServiceContext, id: string): Promise<BranchPaymentTerminal | null> {
|
|
return this.repository.findOne({
|
|
where: { id, tenantId: ctx.tenantId },
|
|
});
|
|
}
|
|
|
|
async findByBranch(ctx: ServiceContext, branchId: string): Promise<BranchPaymentTerminal[]> {
|
|
return this.repository.find({
|
|
where: { branchId, tenantId: ctx.tenantId, isActive: true },
|
|
order: { isPrimary: 'DESC', terminalName: 'ASC' },
|
|
});
|
|
}
|
|
|
|
async findPrimaryTerminal(
|
|
ctx: ServiceContext,
|
|
branchId: string
|
|
): Promise<BranchPaymentTerminal | null> {
|
|
return this.repository.findOne({
|
|
where: { branchId, tenantId: ctx.tenantId, isPrimary: true, isActive: true },
|
|
});
|
|
}
|
|
|
|
async findByTerminalId(
|
|
ctx: ServiceContext,
|
|
terminalId: string
|
|
): Promise<BranchPaymentTerminal | null> {
|
|
return this.repository.findOne({
|
|
where: { terminalId, tenantId: ctx.tenantId },
|
|
});
|
|
}
|
|
|
|
async create(
|
|
ctx: ServiceContext,
|
|
data: CreateBranchPaymentTerminalDto
|
|
): Promise<BranchPaymentTerminal> {
|
|
// If this is the first terminal or marked as primary, ensure only one primary
|
|
if (data.isPrimary) {
|
|
await this.repository.update(
|
|
{ branchId: data.branchId, tenantId: ctx.tenantId, isPrimary: true },
|
|
{ isPrimary: false }
|
|
);
|
|
}
|
|
|
|
const terminal = this.repository.create({
|
|
...data,
|
|
tenantId: ctx.tenantId,
|
|
healthStatus: 'unknown',
|
|
});
|
|
|
|
return this.repository.save(terminal);
|
|
}
|
|
|
|
async update(
|
|
ctx: ServiceContext,
|
|
id: string,
|
|
data: UpdateBranchPaymentTerminalDto
|
|
): Promise<BranchPaymentTerminal | null> {
|
|
const terminal = await this.findById(ctx, id);
|
|
if (!terminal) {
|
|
return null;
|
|
}
|
|
|
|
// If setting as primary, unset other primaries for this branch
|
|
if (data.isPrimary && !terminal.isPrimary) {
|
|
await this.repository.update(
|
|
{ branchId: terminal.branchId, tenantId: ctx.tenantId, isPrimary: true },
|
|
{ isPrimary: false }
|
|
);
|
|
}
|
|
|
|
Object.assign(terminal, data);
|
|
return this.repository.save(terminal);
|
|
}
|
|
|
|
async delete(ctx: ServiceContext, id: string): Promise<boolean> {
|
|
const result = await this.repository.delete({ id, tenantId: ctx.tenantId });
|
|
return result.affected ? result.affected > 0 : false;
|
|
}
|
|
|
|
async setAsPrimary(ctx: ServiceContext, id: string): Promise<BranchPaymentTerminal | null> {
|
|
const terminal = await this.findById(ctx, id);
|
|
if (!terminal) {
|
|
return null;
|
|
}
|
|
|
|
// Unset other primaries
|
|
await this.repository.update(
|
|
{ branchId: terminal.branchId, tenantId: ctx.tenantId, isPrimary: true },
|
|
{ isPrimary: false }
|
|
);
|
|
|
|
terminal.isPrimary = true;
|
|
return this.repository.save(terminal);
|
|
}
|
|
|
|
async updateHealthStatus(
|
|
ctx: ServiceContext,
|
|
id: string,
|
|
healthStatus: HealthStatus
|
|
): Promise<BranchPaymentTerminal | null> {
|
|
const terminal = await this.findById(ctx, id);
|
|
if (!terminal) {
|
|
return null;
|
|
}
|
|
|
|
terminal.healthStatus = healthStatus;
|
|
terminal.lastHealthCheckAt = new Date();
|
|
return this.repository.save(terminal);
|
|
}
|
|
|
|
async recordTransaction(ctx: ServiceContext, id: string): Promise<BranchPaymentTerminal | null> {
|
|
const terminal = await this.findById(ctx, id);
|
|
if (!terminal) {
|
|
return null;
|
|
}
|
|
|
|
terminal.lastTransactionAt = new Date();
|
|
return this.repository.save(terminal);
|
|
}
|
|
|
|
async getHealthSummary(ctx: ServiceContext): Promise<{
|
|
total: number;
|
|
healthy: number;
|
|
degraded: number;
|
|
offline: number;
|
|
unknown: number;
|
|
}> {
|
|
const terminals = await this.repository.find({
|
|
where: { tenantId: ctx.tenantId, isActive: true },
|
|
});
|
|
|
|
return {
|
|
total: terminals.length,
|
|
healthy: terminals.filter(t => t.healthStatus === 'healthy').length,
|
|
degraded: terminals.filter(t => t.healthStatus === 'degraded').length,
|
|
offline: terminals.filter(t => t.healthStatus === 'offline').length,
|
|
unknown: terminals.filter(t => t.healthStatus === 'unknown').length,
|
|
};
|
|
}
|
|
|
|
async getTerminalsNeedingHealthCheck(
|
|
ctx: ServiceContext,
|
|
maxAgeMinutes: number = 30
|
|
): Promise<BranchPaymentTerminal[]> {
|
|
const cutoff = new Date(Date.now() - maxAgeMinutes * 60 * 1000);
|
|
|
|
const terminals = await this.repository.find({
|
|
where: { tenantId: ctx.tenantId, isActive: true },
|
|
});
|
|
|
|
return terminals.filter(t => !t.lastHealthCheckAt || t.lastHealthCheckAt < cutoff);
|
|
}
|
|
}
|