import { query, queryOne, getClient } from '../../config/database.js'; import { NotFoundError, ConflictError, ValidationError } from '../../shared/errors/index.js'; import { taxesService } from '../financial/taxes.service.js'; import { sequencesService, SEQUENCE_CODES } from '../core/sequences.service.js'; import { stockReservationService, ReservationLine } from '../inventory/stock-reservation.service.js'; import { logger } from '../../shared/utils/logger.js'; export interface SalesOrderLine { id: string; order_id: string; product_id: string; product_name?: string; description: string; quantity: number; qty_delivered: number; qty_invoiced: number; uom_id: string; uom_name?: string; price_unit: number; discount: number; tax_ids: string[]; amount_untaxed: number; amount_tax: number; amount_total: number; analytic_account_id?: string; } export interface SalesOrder { id: string; tenant_id: string; company_id: string; company_name?: string; name: string; client_order_ref?: string; partner_id: string; partner_name?: string; order_date: Date; validity_date?: Date; commitment_date?: Date; currency_id: string; currency_code?: string; pricelist_id?: string; pricelist_name?: string; payment_term_id?: string; user_id?: string; user_name?: string; sales_team_id?: string; sales_team_name?: string; amount_untaxed: number; amount_tax: number; amount_total: number; status: 'draft' | 'sent' | 'sale' | 'done' | 'cancelled'; invoice_status: 'pending' | 'partial' | 'invoiced'; delivery_status: 'pending' | 'partial' | 'delivered'; invoice_policy: 'order' | 'delivery'; picking_id?: string; notes?: string; terms_conditions?: string; lines?: SalesOrderLine[]; created_at: Date; confirmed_at?: Date; } export interface CreateSalesOrderDto { company_id: string; partner_id: string; client_order_ref?: string; order_date?: string; validity_date?: string; commitment_date?: string; currency_id: string; pricelist_id?: string; payment_term_id?: string; sales_team_id?: string; invoice_policy?: 'order' | 'delivery'; notes?: string; terms_conditions?: string; } export interface UpdateSalesOrderDto { partner_id?: string; client_order_ref?: string | null; order_date?: string; validity_date?: string | null; commitment_date?: string | null; currency_id?: string; pricelist_id?: string | null; payment_term_id?: string | null; sales_team_id?: string | null; invoice_policy?: 'order' | 'delivery'; notes?: string | null; terms_conditions?: string | null; } export interface CreateSalesOrderLineDto { product_id: string; description: string; quantity: number; uom_id: string; price_unit: number; discount?: number; tax_ids?: string[]; analytic_account_id?: string; } export interface UpdateSalesOrderLineDto { description?: string; quantity?: number; uom_id?: string; price_unit?: number; discount?: number; tax_ids?: string[]; analytic_account_id?: string | null; } export interface SalesOrderFilters { company_id?: string; partner_id?: string; status?: string; invoice_status?: string; delivery_status?: string; date_from?: string; date_to?: string; search?: string; page?: number; limit?: number; } class OrdersService { async findAll(tenantId: string, filters: SalesOrderFilters = {}): Promise<{ data: SalesOrder[]; total: number }> { const { company_id, partner_id, status, invoice_status, delivery_status, date_from, date_to, search, page = 1, limit = 20 } = filters; const offset = (page - 1) * limit; let whereClause = 'WHERE so.tenant_id = $1'; const params: any[] = [tenantId]; let paramIndex = 2; if (company_id) { whereClause += ` AND so.company_id = $${paramIndex++}`; params.push(company_id); } if (partner_id) { whereClause += ` AND so.partner_id = $${paramIndex++}`; params.push(partner_id); } if (status) { whereClause += ` AND so.status = $${paramIndex++}`; params.push(status); } if (invoice_status) { whereClause += ` AND so.invoice_status = $${paramIndex++}`; params.push(invoice_status); } if (delivery_status) { whereClause += ` AND so.delivery_status = $${paramIndex++}`; params.push(delivery_status); } if (date_from) { whereClause += ` AND so.order_date >= $${paramIndex++}`; params.push(date_from); } if (date_to) { whereClause += ` AND so.order_date <= $${paramIndex++}`; params.push(date_to); } if (search) { whereClause += ` AND (so.name ILIKE $${paramIndex} OR so.client_order_ref ILIKE $${paramIndex} OR p.name ILIKE $${paramIndex})`; params.push(`%${search}%`); paramIndex++; } const countResult = await queryOne<{ count: string }>( `SELECT COUNT(*) as count FROM sales.sales_orders so LEFT JOIN core.partners p ON so.partner_id = p.id ${whereClause}`, params ); params.push(limit, offset); const data = await query( `SELECT so.*, c.name as company_name, p.name as partner_name, cu.code as currency_code, pl.name as pricelist_name, u.name as user_name, st.name as sales_team_name FROM sales.sales_orders so LEFT JOIN auth.companies c ON so.company_id = c.id LEFT JOIN core.partners p ON so.partner_id = p.id LEFT JOIN core.currencies cu ON so.currency_id = cu.id LEFT JOIN sales.pricelists pl ON so.pricelist_id = pl.id LEFT JOIN auth.users u ON so.user_id = u.id LEFT JOIN sales.sales_teams st ON so.sales_team_id = st.id ${whereClause} ORDER BY so.created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`, params ); return { data, total: parseInt(countResult?.count || '0', 10), }; } async findById(id: string, tenantId: string): Promise { const order = await queryOne( `SELECT so.*, c.name as company_name, p.name as partner_name, cu.code as currency_code, pl.name as pricelist_name, u.name as user_name, st.name as sales_team_name FROM sales.sales_orders so LEFT JOIN auth.companies c ON so.company_id = c.id LEFT JOIN core.partners p ON so.partner_id = p.id LEFT JOIN core.currencies cu ON so.currency_id = cu.id LEFT JOIN sales.pricelists pl ON so.pricelist_id = pl.id LEFT JOIN auth.users u ON so.user_id = u.id LEFT JOIN sales.sales_teams st ON so.sales_team_id = st.id WHERE so.id = $1 AND so.tenant_id = $2`, [id, tenantId] ); if (!order) { throw new NotFoundError('Orden de venta no encontrada'); } // Get lines const lines = await query( `SELECT sol.*, pr.name as product_name, um.name as uom_name FROM sales.sales_order_lines sol LEFT JOIN inventory.products pr ON sol.product_id = pr.id LEFT JOIN core.uom um ON sol.uom_id = um.id WHERE sol.order_id = $1 ORDER BY sol.created_at`, [id] ); order.lines = lines; return order; } async create(dto: CreateSalesOrderDto, tenantId: string, userId: string): Promise { // Generate sequence number using atomic database function const orderNumber = await sequencesService.getNextNumber(SEQUENCE_CODES.SALES_ORDER, tenantId); const orderDate = dto.order_date || new Date().toISOString().split('T')[0]; const order = await queryOne( `INSERT INTO sales.sales_orders ( tenant_id, company_id, name, client_order_ref, partner_id, order_date, validity_date, commitment_date, currency_id, pricelist_id, payment_term_id, user_id, sales_team_id, invoice_policy, notes, terms_conditions, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING *`, [ tenantId, dto.company_id, orderNumber, dto.client_order_ref, dto.partner_id, orderDate, dto.validity_date, dto.commitment_date, dto.currency_id, dto.pricelist_id, dto.payment_term_id, userId, dto.sales_team_id, dto.invoice_policy || 'order', dto.notes, dto.terms_conditions, userId ] ); return order!; } async update(id: string, dto: UpdateSalesOrderDto, tenantId: string, userId: string): Promise { const existing = await this.findById(id, tenantId); if (existing.status !== 'draft') { throw new ValidationError('Solo se pueden editar órdenes en estado borrador'); } const updateFields: string[] = []; const values: any[] = []; let paramIndex = 1; if (dto.partner_id !== undefined) { updateFields.push(`partner_id = $${paramIndex++}`); values.push(dto.partner_id); } if (dto.client_order_ref !== undefined) { updateFields.push(`client_order_ref = $${paramIndex++}`); values.push(dto.client_order_ref); } if (dto.order_date !== undefined) { updateFields.push(`order_date = $${paramIndex++}`); values.push(dto.order_date); } if (dto.validity_date !== undefined) { updateFields.push(`validity_date = $${paramIndex++}`); values.push(dto.validity_date); } if (dto.commitment_date !== undefined) { updateFields.push(`commitment_date = $${paramIndex++}`); values.push(dto.commitment_date); } if (dto.currency_id !== undefined) { updateFields.push(`currency_id = $${paramIndex++}`); values.push(dto.currency_id); } if (dto.pricelist_id !== undefined) { updateFields.push(`pricelist_id = $${paramIndex++}`); values.push(dto.pricelist_id); } if (dto.payment_term_id !== undefined) { updateFields.push(`payment_term_id = $${paramIndex++}`); values.push(dto.payment_term_id); } if (dto.sales_team_id !== undefined) { updateFields.push(`sales_team_id = $${paramIndex++}`); values.push(dto.sales_team_id); } if (dto.invoice_policy !== undefined) { updateFields.push(`invoice_policy = $${paramIndex++}`); values.push(dto.invoice_policy); } if (dto.notes !== undefined) { updateFields.push(`notes = $${paramIndex++}`); values.push(dto.notes); } if (dto.terms_conditions !== undefined) { updateFields.push(`terms_conditions = $${paramIndex++}`); values.push(dto.terms_conditions); } if (updateFields.length === 0) { return existing; } updateFields.push(`updated_by = $${paramIndex++}`); values.push(userId); updateFields.push(`updated_at = CURRENT_TIMESTAMP`); values.push(id, tenantId); await query( `UPDATE sales.sales_orders SET ${updateFields.join(', ')} WHERE id = $${paramIndex++} AND tenant_id = $${paramIndex}`, values ); return this.findById(id, tenantId); } async delete(id: string, tenantId: string): Promise { const existing = await this.findById(id, tenantId); if (existing.status !== 'draft') { throw new ValidationError('Solo se pueden eliminar órdenes en estado borrador'); } await query( `DELETE FROM sales.sales_orders WHERE id = $1 AND tenant_id = $2`, [id, tenantId] ); } async addLine(orderId: string, dto: CreateSalesOrderLineDto, tenantId: string, userId: string): Promise { const order = await this.findById(orderId, tenantId); if (order.status !== 'draft') { throw new ValidationError('Solo se pueden agregar líneas a órdenes en estado borrador'); } // Calculate amounts with taxes using taxesService const taxResult = await taxesService.calculateTaxes( { quantity: dto.quantity, priceUnit: dto.price_unit, discount: dto.discount || 0, taxIds: dto.tax_ids || [], }, tenantId, 'sales' ); const amountUntaxed = taxResult.amountUntaxed; const amountTax = taxResult.amountTax; const amountTotal = taxResult.amountTotal; const line = await queryOne( `INSERT INTO sales.sales_order_lines ( order_id, tenant_id, product_id, description, quantity, uom_id, price_unit, discount, tax_ids, amount_untaxed, amount_tax, amount_total, analytic_account_id ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *`, [ orderId, tenantId, dto.product_id, dto.description, dto.quantity, dto.uom_id, dto.price_unit, dto.discount || 0, dto.tax_ids || [], amountUntaxed, amountTax, amountTotal, dto.analytic_account_id ] ); // Update order totals await this.updateTotals(orderId); return line!; } async updateLine(orderId: string, lineId: string, dto: UpdateSalesOrderLineDto, tenantId: string): Promise { const order = await this.findById(orderId, tenantId); if (order.status !== 'draft') { throw new ValidationError('Solo se pueden editar líneas de órdenes en estado borrador'); } const existingLine = order.lines?.find(l => l.id === lineId); if (!existingLine) { throw new NotFoundError('Línea de orden no encontrada'); } const updateFields: string[] = []; const values: any[] = []; let paramIndex = 1; const quantity = dto.quantity ?? existingLine.quantity; const priceUnit = dto.price_unit ?? existingLine.price_unit; const discount = dto.discount ?? existingLine.discount; if (dto.description !== undefined) { updateFields.push(`description = $${paramIndex++}`); values.push(dto.description); } if (dto.quantity !== undefined) { updateFields.push(`quantity = $${paramIndex++}`); values.push(dto.quantity); } if (dto.uom_id !== undefined) { updateFields.push(`uom_id = $${paramIndex++}`); values.push(dto.uom_id); } if (dto.price_unit !== undefined) { updateFields.push(`price_unit = $${paramIndex++}`); values.push(dto.price_unit); } if (dto.discount !== undefined) { updateFields.push(`discount = $${paramIndex++}`); values.push(dto.discount); } if (dto.tax_ids !== undefined) { updateFields.push(`tax_ids = $${paramIndex++}`); values.push(dto.tax_ids); } if (dto.analytic_account_id !== undefined) { updateFields.push(`analytic_account_id = $${paramIndex++}`); values.push(dto.analytic_account_id); } // Recalculate amounts const subtotal = quantity * priceUnit; const discountAmount = subtotal * discount / 100; const amountUntaxed = subtotal - discountAmount; const amountTax = 0; // TODO: Calculate taxes const amountTotal = amountUntaxed + amountTax; updateFields.push(`amount_untaxed = $${paramIndex++}`); values.push(amountUntaxed); updateFields.push(`amount_tax = $${paramIndex++}`); values.push(amountTax); updateFields.push(`amount_total = $${paramIndex++}`); values.push(amountTotal); updateFields.push(`updated_at = CURRENT_TIMESTAMP`); values.push(lineId, orderId); await query( `UPDATE sales.sales_order_lines SET ${updateFields.join(', ')} WHERE id = $${paramIndex++} AND order_id = $${paramIndex}`, values ); // Update order totals await this.updateTotals(orderId); const updated = await queryOne( `SELECT * FROM sales.sales_order_lines WHERE id = $1`, [lineId] ); return updated!; } async removeLine(orderId: string, lineId: string, tenantId: string): Promise { const order = await this.findById(orderId, tenantId); if (order.status !== 'draft') { throw new ValidationError('Solo se pueden eliminar líneas de órdenes en estado borrador'); } await query( `DELETE FROM sales.sales_order_lines WHERE id = $1 AND order_id = $2`, [lineId, orderId] ); // Update order totals await this.updateTotals(orderId); } async confirm(id: string, tenantId: string, userId: string): Promise { const order = await this.findById(id, tenantId); if (order.status !== 'draft') { throw new ValidationError('Solo se pueden confirmar órdenes en estado borrador'); } if (!order.lines || order.lines.length === 0) { throw new ValidationError('La orden debe tener al menos una línea'); } const client = await getClient(); try { await client.query('BEGIN'); // Get default outgoing location for the company const locationResult = await client.query( `SELECT l.id as location_id, w.id as warehouse_id FROM inventory.locations l INNER JOIN inventory.warehouses w ON l.warehouse_id = w.id WHERE w.tenant_id = $1 AND w.company_id = $2 AND l.location_type = 'internal' AND l.active = true ORDER BY w.is_default DESC, l.name ASC LIMIT 1`, [tenantId, order.company_id] ); const sourceLocationId = locationResult.rows[0]?.location_id; const warehouseId = locationResult.rows[0]?.warehouse_id; if (!sourceLocationId) { throw new ValidationError('No hay ubicación de stock configurada para esta empresa'); } // Get customer location (or create virtual one) const custLocationResult = await client.query( `SELECT id FROM inventory.locations WHERE tenant_id = $1 AND location_type = 'customer' LIMIT 1`, [tenantId] ); let customerLocationId = custLocationResult.rows[0]?.id; if (!customerLocationId) { // Create a default customer location const newLocResult = await client.query( `INSERT INTO inventory.locations (tenant_id, name, location_type, active) VALUES ($1, 'Customers', 'customer', true) RETURNING id`, [tenantId] ); customerLocationId = newLocResult.rows[0].id; } // TASK-003-04: Reserve stock for order lines const reservationLines: ReservationLine[] = order.lines.map(line => ({ productId: line.product_id, locationId: sourceLocationId, quantity: line.quantity, })); const reservationResult = await stockReservationService.reserveWithClient( client, reservationLines, tenantId, order.name, false // Don't allow partial - fail if insufficient stock ); if (!reservationResult.success) { throw new ValidationError( `Stock insuficiente: ${reservationResult.errors.join(', ')}` ); } // TASK-003-03: Create outgoing picking const pickingNumber = await sequencesService.getNextNumber(SEQUENCE_CODES.PICKING_OUT, tenantId); const pickingResult = await client.query( `INSERT INTO inventory.pickings ( tenant_id, company_id, name, picking_type, location_id, location_dest_id, partner_id, scheduled_date, origin, status, created_by ) VALUES ($1, $2, $3, 'outgoing', $4, $5, $6, $7, $8, 'confirmed', $9) RETURNING id`, [ tenantId, order.company_id, pickingNumber, sourceLocationId, customerLocationId, order.partner_id, order.commitment_date || new Date().toISOString().split('T')[0], order.name, // origin = sales order reference userId, ] ); const pickingId = pickingResult.rows[0].id; // Create stock moves for each order line for (const line of order.lines) { await client.query( `INSERT INTO inventory.stock_moves ( tenant_id, picking_id, product_id, product_uom_id, location_id, location_dest_id, product_qty, status, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'confirmed', $8)`, [ tenantId, pickingId, line.product_id, line.uom_id, sourceLocationId, customerLocationId, line.quantity, userId, ] ); } // Update order: status to 'sent', link picking await client.query( `UPDATE sales.sales_orders SET status = 'sent', picking_id = $1, confirmed_at = CURRENT_TIMESTAMP, confirmed_by = $2, updated_by = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3`, [pickingId, userId, id] ); await client.query('COMMIT'); logger.info('Sales order confirmed with picking', { orderId: id, orderName: order.name, pickingId, pickingName: pickingNumber, linesCount: order.lines.length, tenantId, }); return this.findById(id, tenantId); } catch (error) { await client.query('ROLLBACK'); logger.error('Error confirming sales order', { error: (error as Error).message, orderId: id, tenantId, }); throw error; } finally { client.release(); } } async cancel(id: string, tenantId: string, userId: string): Promise { const order = await this.findById(id, tenantId); if (order.status === 'done') { throw new ValidationError('No se pueden cancelar órdenes completadas'); } if (order.status === 'cancelled') { throw new ValidationError('La orden ya está cancelada'); } // Check if there are any deliveries or invoices if (order.delivery_status !== 'pending') { throw new ValidationError('No se puede cancelar: ya hay entregas asociadas'); } if (order.invoice_status !== 'pending') { throw new ValidationError('No se puede cancelar: ya hay facturas asociadas'); } const client = await getClient(); try { await client.query('BEGIN'); // Release stock reservations if order was confirmed if (order.status === 'sent' || order.status === 'sale') { // Get the source location from picking if (order.picking_id) { const pickingResult = await client.query( `SELECT location_id FROM inventory.pickings WHERE id = $1`, [order.picking_id] ); const sourceLocationId = pickingResult.rows[0]?.location_id; if (sourceLocationId && order.lines) { const releaseLines: ReservationLine[] = order.lines.map(line => ({ productId: line.product_id, locationId: sourceLocationId, quantity: line.quantity, })); await stockReservationService.releaseWithClient( client, releaseLines, tenantId ); } // Cancel the picking await client.query( `UPDATE inventory.pickings SET status = 'cancelled', updated_by = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2`, [userId, order.picking_id] ); await client.query( `UPDATE inventory.stock_moves SET status = 'cancelled', updated_by = $1, updated_at = CURRENT_TIMESTAMP WHERE picking_id = $2`, [userId, order.picking_id] ); } } // Update order status await client.query( `UPDATE sales.sales_orders SET status = 'cancelled', cancelled_at = CURRENT_TIMESTAMP, cancelled_by = $1, updated_by = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND tenant_id = $3`, [userId, id, tenantId] ); await client.query('COMMIT'); logger.info('Sales order cancelled', { orderId: id, orderName: order.name, tenantId, }); return this.findById(id, tenantId); } catch (error) { await client.query('ROLLBACK'); logger.error('Error cancelling sales order', { error: (error as Error).message, orderId: id, tenantId, }); throw error; } finally { client.release(); } } async createInvoice(id: string, tenantId: string, userId: string): Promise<{ orderId: string; invoiceId: string }> { const order = await this.findById(id, tenantId); if (order.status !== 'sent' && order.status !== 'sale' && order.status !== 'done') { throw new ValidationError('Solo se pueden facturar órdenes confirmadas (sent/sale)'); } if (order.invoice_status === 'invoiced') { throw new ValidationError('La orden ya está completamente facturada'); } // Check if there are quantities to invoice const linesToInvoice = order.lines?.filter(l => { if (order.invoice_policy === 'order') { return l.quantity > l.qty_invoiced; } else { return l.qty_delivered > l.qty_invoiced; } }); if (!linesToInvoice || linesToInvoice.length === 0) { throw new ValidationError('No hay líneas para facturar'); } const client = await getClient(); try { await client.query('BEGIN'); // Generate invoice number const seqResult = await client.query( `SELECT COALESCE(MAX(CAST(SUBSTRING(name FROM 5) AS INTEGER)), 0) + 1 as next_num FROM financial.invoices WHERE tenant_id = $1 AND name LIKE 'INV-%'`, [tenantId] ); const invoiceNumber = `INV-${String(seqResult.rows[0]?.next_num || 1).padStart(6, '0')}`; // Create invoice const invoiceResult = await client.query( `INSERT INTO financial.invoices ( tenant_id, company_id, name, partner_id, invoice_date, due_date, currency_id, invoice_type, amount_untaxed, amount_tax, amount_total, source_document, created_by ) VALUES ($1, $2, $3, $4, CURRENT_DATE, CURRENT_DATE + INTERVAL '30 days', $5, 'customer', 0, 0, 0, $6, $7) RETURNING id`, [tenantId, order.company_id, invoiceNumber, order.partner_id, order.currency_id, order.name, userId] ); const invoiceId = invoiceResult.rows[0].id; // Create invoice lines and update qty_invoiced for (const line of linesToInvoice) { const qtyToInvoice = order.invoice_policy === 'order' ? line.quantity - line.qty_invoiced : line.qty_delivered - line.qty_invoiced; const lineAmount = qtyToInvoice * line.price_unit * (1 - line.discount / 100); await client.query( `INSERT INTO financial.invoice_lines ( invoice_id, tenant_id, product_id, description, quantity, uom_id, price_unit, discount, amount_untaxed, amount_tax, amount_total ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 0, $9)`, [invoiceId, tenantId, line.product_id, line.description, qtyToInvoice, line.uom_id, line.price_unit, line.discount, lineAmount] ); await client.query( `UPDATE sales.sales_order_lines SET qty_invoiced = qty_invoiced + $1 WHERE id = $2`, [qtyToInvoice, line.id] ); } // Update invoice totals await client.query( `UPDATE financial.invoices SET amount_untaxed = (SELECT COALESCE(SUM(amount_untaxed), 0) FROM financial.invoice_lines WHERE invoice_id = $1), amount_total = (SELECT COALESCE(SUM(amount_total), 0) FROM financial.invoice_lines WHERE invoice_id = $1) WHERE id = $1`, [invoiceId] ); // Update order invoice_status await client.query( `UPDATE sales.sales_orders SET invoice_status = CASE WHEN (SELECT SUM(qty_invoiced) FROM sales.sales_order_lines WHERE order_id = $1) >= (SELECT SUM(quantity) FROM sales.sales_order_lines WHERE order_id = $1) THEN 'invoiced'::sales.invoice_status ELSE 'partial'::sales.invoice_status END, updated_by = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1`, [id, userId] ); await client.query('COMMIT'); return { orderId: id, invoiceId }; } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } private async updateTotals(orderId: string): Promise { await query( `UPDATE sales.sales_orders SET amount_untaxed = COALESCE((SELECT SUM(amount_untaxed) FROM sales.sales_order_lines WHERE order_id = $1), 0), amount_tax = COALESCE((SELECT SUM(amount_tax) FROM sales.sales_order_lines WHERE order_id = $1), 0), amount_total = COALESCE((SELECT SUM(amount_total) FROM sales.sales_order_lines WHERE order_id = $1), 0) WHERE id = $1`, [orderId] ); } } export const ordersService = new OrdersService();