feat: invoice
This commit is contained in:
parent
92a54866b4
commit
80e7a96bce
@ -4,8 +4,13 @@ const service = new AbonemmentService();
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
async subscribe(req, res) {
|
async subscribe(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const rawUser = req.user || {};
|
||||||
|
console.log('[CONTROLLER SUBSCRIBE] Raw req.user:', { id: rawUser.id, userId: rawUser.userId, email: rawUser.email, role: rawUser.role });
|
||||||
|
const actorUser = { ...rawUser, id: rawUser.id ?? rawUser.userId ?? null };
|
||||||
|
console.log('[CONTROLLER SUBSCRIBE] Normalized actorUser:', { id: actorUser.id, email: actorUser.email, role: actorUser.role });
|
||||||
|
|
||||||
const result = await service.subscribeOrder({
|
const result = await service.subscribeOrder({
|
||||||
userId: req.user?.id || null,
|
userId: actorUser.id || null,
|
||||||
items: req.body.items,
|
items: req.body.items,
|
||||||
billingInterval: req.body.billing_interval,
|
billingInterval: req.body.billing_interval,
|
||||||
intervalCount: req.body.interval_count,
|
intervalCount: req.body.interval_count,
|
||||||
@ -19,8 +24,8 @@ module.exports = {
|
|||||||
country: req.body.country,
|
country: req.body.country,
|
||||||
frequency: req.body.frequency,
|
frequency: req.body.frequency,
|
||||||
startDate: req.body.startDate,
|
startDate: req.body.startDate,
|
||||||
actorUser: req.user,
|
actorUser, // normalized to include id
|
||||||
referredBy: req.body.referred_by, // NEW: Pass referred_by from frontend
|
referredBy: req.body.referred_by,
|
||||||
});
|
});
|
||||||
return res.json({ success: true, data: result });
|
return res.json({ success: true, data: result });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -31,7 +36,9 @@ module.exports = {
|
|||||||
|
|
||||||
async pause(req, res) {
|
async pause(req, res) {
|
||||||
try {
|
try {
|
||||||
const data = await service.pause({ abonementId: req.params.id, actorUser: req.user });
|
const rawUser = req.user || {};
|
||||||
|
const actorUser = { ...rawUser, id: rawUser.id ?? rawUser.userId ?? null };
|
||||||
|
const data = await service.pause({ abonementId: req.params.id, actorUser });
|
||||||
return res.json({ success: true, data });
|
return res.json({ success: true, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ABONEMENT PAUSE]', err);
|
console.error('[ABONEMENT PAUSE]', err);
|
||||||
@ -41,7 +48,9 @@ module.exports = {
|
|||||||
|
|
||||||
async resume(req, res) {
|
async resume(req, res) {
|
||||||
try {
|
try {
|
||||||
const data = await service.resume({ abonementId: req.params.id, actorUser: req.user });
|
const rawUser = req.user || {};
|
||||||
|
const actorUser = { ...rawUser, id: rawUser.id ?? rawUser.userId ?? null };
|
||||||
|
const data = await service.resume({ abonementId: req.params.id, actorUser });
|
||||||
return res.json({ success: true, data });
|
return res.json({ success: true, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ABONEMENT RESUME]', err);
|
console.error('[ABONEMENT RESUME]', err);
|
||||||
@ -51,7 +60,9 @@ module.exports = {
|
|||||||
|
|
||||||
async cancel(req, res) {
|
async cancel(req, res) {
|
||||||
try {
|
try {
|
||||||
const data = await service.cancel({ abonementId: req.params.id, actorUser: req.user });
|
const rawUser = req.user || {};
|
||||||
|
const actorUser = { ...rawUser, id: rawUser.id ?? rawUser.userId ?? null };
|
||||||
|
const data = await service.cancel({ abonementId: req.params.id, actorUser });
|
||||||
return res.json({ success: true, data });
|
return res.json({ success: true, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ABONEMENT CANCEL]', err);
|
console.error('[ABONEMENT CANCEL]', err);
|
||||||
@ -61,7 +72,9 @@ module.exports = {
|
|||||||
|
|
||||||
async renew(req, res) {
|
async renew(req, res) {
|
||||||
try {
|
try {
|
||||||
const data = await service.adminRenew({ abonementId: req.params.id, actorUser: req.user });
|
const rawUser = req.user || {};
|
||||||
|
const actorUser = { ...rawUser, id: rawUser.id ?? rawUser.userId ?? null };
|
||||||
|
const data = await service.adminRenew({ abonementId: req.params.id, actorUser });
|
||||||
return res.json({ success: true, data });
|
return res.json({ success: true, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ABONEMENT RENEW]', err);
|
console.error('[ABONEMENT RENEW]', err);
|
||||||
@ -71,7 +84,10 @@ module.exports = {
|
|||||||
|
|
||||||
async getMine(req, res) {
|
async getMine(req, res) {
|
||||||
try {
|
try {
|
||||||
const data = await service.getMyAbonements({ userId: req.user.id });
|
const rawUser = req.user || {};
|
||||||
|
const id = rawUser.id ?? rawUser.userId;
|
||||||
|
console.log('[CONTROLLER GET MINE] Using user id:', id);
|
||||||
|
const data = await service.getMyAbonements({ userId: id });
|
||||||
return res.json({ success: true, data });
|
return res.json({ success: true, data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ABONEMENT MINE]', err);
|
console.error('[ABONEMENT MINE]', err);
|
||||||
@ -81,8 +97,7 @@ module.exports = {
|
|||||||
|
|
||||||
async getHistory(req, res) {
|
async getHistory(req, res) {
|
||||||
try {
|
try {
|
||||||
const data = await service.getHistory({ abonementId: req.params.id });
|
return res.json({ success: true, data: await service.getHistory({ abonementId: req.params.id }) });
|
||||||
return res.json({ success: true, data });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ABONEMENT HISTORY]', err);
|
console.error('[ABONEMENT HISTORY]', err);
|
||||||
return res.status(400).json({ success: false, message: err.message });
|
return res.status(400).json({ success: false, message: err.message });
|
||||||
@ -91,8 +106,7 @@ module.exports = {
|
|||||||
|
|
||||||
async adminList(req, res) {
|
async adminList(req, res) {
|
||||||
try {
|
try {
|
||||||
const data = await service.adminList({ status: req.query.status });
|
return res.json({ success: true, data: await service.adminList({ status: req.query.status }) });
|
||||||
return res.json({ success: true, data });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ABONEMENT ADMIN LIST]', err);
|
console.error('[ABONEMENT ADMIN LIST]', err);
|
||||||
return res.status(403).json({ success: false, message: err.message });
|
return res.status(403).json({ success: false, message: err.message });
|
||||||
@ -101,11 +115,9 @@ module.exports = {
|
|||||||
|
|
||||||
async getReferredSubscriptions(req, res) {
|
async getReferredSubscriptions(req, res) {
|
||||||
try {
|
try {
|
||||||
const data = await service.getReferredSubscriptions({
|
const rawUser = req.user || {};
|
||||||
userId: req.user.id,
|
const id = rawUser.id ?? rawUser.userId;
|
||||||
email: req.user.email,
|
return res.json({ success: true, data: await service.getReferredSubscriptions({ userId: id, email: rawUser.email }) });
|
||||||
});
|
|
||||||
return res.json({ success: true, data });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ABONEMENT REFERRED SUBSCRIPTIONS]', err);
|
console.error('[ABONEMENT REFERRED SUBSCRIPTIONS]', err);
|
||||||
return res.status(400).json({ success: false, message: err.message });
|
return res.status(400).json({ success: false, message: err.message });
|
||||||
|
|||||||
48
controller/invoice/InvoiceController.js
Normal file
48
controller/invoice/InvoiceController.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
const InvoiceService = require('../../services/invoice/InvoiceService');
|
||||||
|
const service = new InvoiceService();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async listMine(req, res) {
|
||||||
|
try {
|
||||||
|
const data = await service.listMine(req.user.id, {
|
||||||
|
status: req.query.status,
|
||||||
|
limit: Number(req.query.limit || 50),
|
||||||
|
offset: Number(req.query.offset || 0),
|
||||||
|
});
|
||||||
|
return res.json({ success: true, data });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[INVOICE LIST MINE]', e);
|
||||||
|
return res.status(400).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async adminList(req, res) {
|
||||||
|
try {
|
||||||
|
const data = await service.adminList({
|
||||||
|
status: req.query.status,
|
||||||
|
limit: Number(req.query.limit || 200),
|
||||||
|
offset: Number(req.query.offset || 0),
|
||||||
|
});
|
||||||
|
return res.json({ success: true, data });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[INVOICE ADMIN LIST]', e);
|
||||||
|
return res.status(403).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async pay(req, res) {
|
||||||
|
try {
|
||||||
|
const data = await service.markPaid(req.params.id, {
|
||||||
|
payment_method: req.body.payment_method,
|
||||||
|
transaction_id: req.body.transaction_id,
|
||||||
|
amount: req.body.amount,
|
||||||
|
paid_at: req.body.paid_at ? new Date(req.body.paid_at) : undefined,
|
||||||
|
details: req.body.details,
|
||||||
|
});
|
||||||
|
return res.json({ success: true, data });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[INVOICE PAY]', e);
|
||||||
|
return res.status(400).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -473,6 +473,27 @@ async function createDatabase() {
|
|||||||
`);
|
`);
|
||||||
console.log('✅ Referral token usage table created/verified');
|
console.log('✅ Referral token usage table created/verified');
|
||||||
|
|
||||||
|
// --- Affiliates table (for Affiliate Manager) ---
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS affiliates (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
url VARCHAR(1024) NOT NULL,
|
||||||
|
object_storage_id VARCHAR(255) NULL,
|
||||||
|
original_filename VARCHAR(255) NULL,
|
||||||
|
category VARCHAR(128) NOT NULL,
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
commission_rate DECIMAL(5,2) NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_affiliates_created (created_at),
|
||||||
|
INDEX idx_affiliates_active (is_active),
|
||||||
|
INDEX idx_affiliates_category (category)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('✅ Affiliates table created/verified');
|
||||||
|
|
||||||
// --- Authorization Tables ---
|
// --- Authorization Tables ---
|
||||||
|
|
||||||
// 14. permissions table: Defines granular permissions
|
// 14. permissions table: Defines granular permissions
|
||||||
@ -699,6 +720,101 @@ async function createDatabase() {
|
|||||||
`);
|
`);
|
||||||
console.log('✅ Coffee abonement history table created/verified');
|
console.log('✅ Coffee abonement history table created/verified');
|
||||||
|
|
||||||
|
// --- Invoices: unified for subscriptions and shop orders ---
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS invoices (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
invoice_number VARCHAR(64) NOT NULL UNIQUE,
|
||||||
|
user_id INT NULL,
|
||||||
|
source_type ENUM('subscription','shop') NOT NULL,
|
||||||
|
source_id BIGINT NOT NULL,
|
||||||
|
buyer_name VARCHAR(255) NULL,
|
||||||
|
buyer_email VARCHAR(255) NULL,
|
||||||
|
buyer_street VARCHAR(255) NULL,
|
||||||
|
buyer_postal_code VARCHAR(20) NULL,
|
||||||
|
buyer_city VARCHAR(100) NULL,
|
||||||
|
buyer_country VARCHAR(100) NULL,
|
||||||
|
currency CHAR(3) NOT NULL,
|
||||||
|
total_net DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
total_tax DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
total_gross DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
vat_rate DECIMAL(6,3) NULL,
|
||||||
|
status ENUM('draft','issued','paid','canceled') NOT NULL DEFAULT 'draft',
|
||||||
|
issued_at DATETIME NULL,
|
||||||
|
due_at DATETIME NULL,
|
||||||
|
pdf_storage_key VARCHAR(255) NULL,
|
||||||
|
context JSON NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT fk_invoice_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
INDEX idx_invoice_source (source_type, source_id),
|
||||||
|
INDEX idx_invoice_issued (issued_at),
|
||||||
|
INDEX idx_invoice_status (status)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('✅ Invoices table created/verified');
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS invoice_items (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
invoice_id BIGINT NOT NULL,
|
||||||
|
product_id BIGINT NULL,
|
||||||
|
sku VARCHAR(128) NULL,
|
||||||
|
description VARCHAR(512) NOT NULL,
|
||||||
|
quantity DECIMAL(12,3) NOT NULL DEFAULT 1.000,
|
||||||
|
unit_price DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
tax_rate DECIMAL(6,3) NULL,
|
||||||
|
line_net DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
line_tax DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
line_gross DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT fk_item_invoice FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
INDEX idx_item_invoice (invoice_id)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('✅ Invoice items table created/verified');
|
||||||
|
|
||||||
|
await connection.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS invoice_payments (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
invoice_id BIGINT NOT NULL,
|
||||||
|
payment_method VARCHAR(64) NOT NULL,
|
||||||
|
transaction_id VARCHAR(128) NULL,
|
||||||
|
amount DECIMAL(12,2) NOT NULL,
|
||||||
|
paid_at DATETIME NULL,
|
||||||
|
status ENUM('succeeded','pending','failed','refunded') NOT NULL DEFAULT 'pending',
|
||||||
|
details JSON NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT fk_payment_invoice FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
INDEX idx_payment_invoice (invoice_id),
|
||||||
|
INDEX idx_payment_status (status),
|
||||||
|
INDEX idx_payment_paid_at (paid_at)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
console.log('✅ Invoice payments table created/verified');
|
||||||
|
|
||||||
|
// Extend coffee_abonement_history with optional related_invoice_id
|
||||||
|
try {
|
||||||
|
await connection.query(`ALTER TABLE coffee_abonement_history ADD COLUMN related_invoice_id BIGINT NULL`);
|
||||||
|
console.log('🆕 Added coffee_abonement_history.related_invoice_id');
|
||||||
|
} catch (e) {
|
||||||
|
if (!/Duplicate column name|exists/i.test(e.message)) {
|
||||||
|
console.log('ℹ️ related_invoice_id add skipped or not required:', e.message);
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ coffee_abonement_history.related_invoice_id already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await connection.query(`
|
||||||
|
ALTER TABLE coffee_abonement_history
|
||||||
|
ADD CONSTRAINT fk_hist_invoice FOREIGN KEY (related_invoice_id)
|
||||||
|
REFERENCES invoices(id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
`);
|
||||||
|
console.log('🆕 Added FK fk_hist_invoice on coffee_abonement_history.related_invoice_id');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('ℹ️ fk_hist_invoice already exists or failed:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Matrix: Global 5-ary tree config and relations ---
|
// --- Matrix: Global 5-ary tree config and relations ---
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
CREATE TABLE IF NOT EXISTS matrix_config (
|
CREATE TABLE IF NOT EXISTS matrix_config (
|
||||||
|
|||||||
1
debug-pdf/template_1_html_full.html
Normal file
1
debug-pdf/template_1_html_full.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
safdasdffasdafd
|
||||||
1
debug-pdf/template_1_html_raw.bin
Normal file
1
debug-pdf/template_1_html_raw.bin
Normal file
@ -0,0 +1 @@
|
|||||||
|
safdasdffasdafd
|
||||||
1
debug-pdf/template_1_sanitized_preview.html
Normal file
1
debug-pdf/template_1_sanitized_preview.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
safdasdffasdafd
|
||||||
@ -22,6 +22,8 @@ class Abonemment {
|
|||||||
this.country = row.country;
|
this.country = row.country;
|
||||||
this.frequency = row.frequency;
|
this.frequency = row.frequency;
|
||||||
this.referred_by = row.referred_by; // NEW
|
this.referred_by = row.referred_by; // NEW
|
||||||
|
this.purchaser_user_id = row.purchaser_user_id ?? null; // NEW
|
||||||
|
this.user_id = row.user_id ?? null; // NEW: map owner user_id
|
||||||
this.created_at = row.created_at;
|
this.created_at = row.created_at;
|
||||||
this.updated_at = row.updated_at;
|
this.updated_at = row.updated_at;
|
||||||
}
|
}
|
||||||
|
|||||||
29
models/Invoice.js
Normal file
29
models/Invoice.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
class Invoice {
|
||||||
|
constructor(row) {
|
||||||
|
this.id = row.id;
|
||||||
|
this.invoice_number = row.invoice_number;
|
||||||
|
this.user_id = row.user_id;
|
||||||
|
this.source_type = row.source_type;
|
||||||
|
this.source_id = row.source_id;
|
||||||
|
this.buyer_name = row.buyer_name;
|
||||||
|
this.buyer_email = row.buyer_email;
|
||||||
|
this.buyer_street = row.buyer_street;
|
||||||
|
this.buyer_postal_code = row.buyer_postal_code;
|
||||||
|
this.buyer_city = row.buyer_city;
|
||||||
|
this.buyer_country = row.buyer_country;
|
||||||
|
this.currency = row.currency;
|
||||||
|
this.total_net = Number(row.total_net);
|
||||||
|
this.total_tax = Number(row.total_tax);
|
||||||
|
this.total_gross = Number(row.total_gross);
|
||||||
|
this.vat_rate = row.vat_rate != null ? Number(row.vat_rate) : null;
|
||||||
|
this.status = row.status;
|
||||||
|
this.issued_at = row.issued_at;
|
||||||
|
this.due_at = row.due_at;
|
||||||
|
this.pdf_storage_key = row.pdf_storage_key;
|
||||||
|
this.context = row.context ? (typeof row.context === 'string' ? JSON.parse(row.context) : row.context) : null;
|
||||||
|
this.created_at = row.created_at;
|
||||||
|
this.updated_at = row.updated_at;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Invoice;
|
||||||
@ -2,15 +2,38 @@ const pool = require('../../database/database');
|
|||||||
const Abonemment = require('../../models/Abonemment');
|
const Abonemment = require('../../models/Abonemment');
|
||||||
|
|
||||||
class AbonemmentRepository {
|
class AbonemmentRepository {
|
||||||
|
// NEW: cache table columns once per process
|
||||||
|
static _columnsCache = null;
|
||||||
|
|
||||||
|
// NEW: load columns for coffee_abonements
|
||||||
|
async loadColumns() {
|
||||||
|
if (AbonemmentRepository._columnsCache) return AbonemmentRepository._columnsCache;
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'coffee_abonements'`
|
||||||
|
);
|
||||||
|
const set = new Set(rows.map(r => r.COLUMN_NAME));
|
||||||
|
AbonemmentRepository._columnsCache = set;
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: helpers to include fields conditionally
|
||||||
|
async hasColumn(name) {
|
||||||
|
const cols = await this.loadColumns();
|
||||||
|
return cols.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
async createAbonement(referredBy, snapshot) {
|
async createAbonement(referredBy, snapshot) {
|
||||||
const conn = await pool.getConnection();
|
const conn = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
await conn.beginTransaction();
|
await conn.beginTransaction();
|
||||||
const [res] = await conn.query(
|
|
||||||
`INSERT INTO coffee_abonements
|
// NEW: dynamically assemble column list
|
||||||
(pack_group, status, started_at, ended_at, next_billing_at, billing_interval, interval_count, price, currency, is_auto_renew, notes, pack_breakdown, first_name, last_name, email, street, postal_code, city, country, frequency, referred_by, created_at, updated_at)
|
const cols = [
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
'pack_group','status','started_at','ended_at','next_billing_at','billing_interval','interval_count',
|
||||||
[
|
'price','currency','is_auto_renew','notes','pack_breakdown','first_name','last_name','email','street',
|
||||||
|
'postal_code','city','country','frequency','referred_by'
|
||||||
|
];
|
||||||
|
const vals = [
|
||||||
snapshot.pack_group || '',
|
snapshot.pack_group || '',
|
||||||
snapshot.status || 'active',
|
snapshot.status || 'active',
|
||||||
snapshot.started_at,
|
snapshot.started_at,
|
||||||
@ -31,10 +54,42 @@ class AbonemmentRepository {
|
|||||||
snapshot.city || null,
|
snapshot.city || null,
|
||||||
snapshot.country || null,
|
snapshot.country || null,
|
||||||
snapshot.frequency || null,
|
snapshot.frequency || null,
|
||||||
referredBy || null, // Ensure referred_by is stored
|
referredBy || null,
|
||||||
],
|
];
|
||||||
|
|
||||||
|
console.log('[CREATE ABONEMENT] Incoming snapshot user linking:', {
|
||||||
|
actor_user_id: snapshot.actor_user_id,
|
||||||
|
user_id: snapshot.user_id,
|
||||||
|
purchaser_user_id: snapshot.purchaser_user_id,
|
||||||
|
referred_by: referredBy,
|
||||||
|
});
|
||||||
|
|
||||||
|
// NEW: optionally include user_id and purchaser_user_id
|
||||||
|
if (await this.hasColumn('user_id')) {
|
||||||
|
cols.push('user_id');
|
||||||
|
vals.push(snapshot.user_id ?? null);
|
||||||
|
}
|
||||||
|
if (await this.hasColumn('purchaser_user_id')) {
|
||||||
|
cols.push('purchaser_user_id');
|
||||||
|
vals.push(snapshot.purchaser_user_id ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholders = cols.map(() => '?').join(', ');
|
||||||
|
console.log('[CREATE ABONEMENT] Final columns:', cols);
|
||||||
|
console.log('[CREATE ABONEMENT] Final values preview:', {
|
||||||
|
pack_group: vals[0],
|
||||||
|
status: vals[1],
|
||||||
|
user_id: cols.includes('user_id') ? vals[cols.indexOf('user_id')] : undefined,
|
||||||
|
purchaser_user_id: cols.includes('purchaser_user_id') ? vals[cols.indexOf('purchaser_user_id')] : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [res] = await conn.query(
|
||||||
|
`INSERT INTO coffee_abonements (${cols.join(', ')}, created_at, updated_at)
|
||||||
|
VALUES (${placeholders}, NOW(), NOW())`,
|
||||||
|
vals
|
||||||
);
|
);
|
||||||
const abonementId = res.insertId;
|
const abonementId = res.insertId;
|
||||||
|
console.log('[CREATE ABONEMENT] Inserted abonement ID:', abonementId);
|
||||||
|
|
||||||
const historyDetails = { ...(snapshot.details || {}), pack_group: snapshot.pack_group };
|
const historyDetails = { ...(snapshot.details || {}), pack_group: snapshot.pack_group };
|
||||||
await conn.query(
|
await conn.query(
|
||||||
@ -45,13 +100,21 @@ class AbonemmentRepository {
|
|||||||
abonementId,
|
abonementId,
|
||||||
'created',
|
'created',
|
||||||
snapshot.started_at,
|
snapshot.started_at,
|
||||||
referredBy || snapshot.actor_user_id || null, // Use referredBy as actor_user_id if available
|
referredBy || snapshot.actor_user_id || null,
|
||||||
JSON.stringify(historyDetails),
|
JSON.stringify(historyDetails),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
await conn.commit();
|
await conn.commit();
|
||||||
return this.getAbonementById(abonementId);
|
const created = await this.getAbonementById(abonementId);
|
||||||
|
console.log('[CREATE ABONEMENT] Loaded abonement row after insert:', {
|
||||||
|
id: created?.id,
|
||||||
|
user_id: created?.user_id,
|
||||||
|
purchaser_user_id: created?.purchaser_user_id,
|
||||||
|
pack_group: created?.pack_group,
|
||||||
|
status: created?.status,
|
||||||
|
});
|
||||||
|
return created;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await conn.rollback();
|
await conn.rollback();
|
||||||
throw err;
|
throw err;
|
||||||
@ -218,24 +281,27 @@ class AbonemmentRepository {
|
|||||||
const conn = await pool.getConnection();
|
const conn = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
await conn.beginTransaction();
|
await conn.beginTransaction();
|
||||||
await conn.query(
|
|
||||||
`UPDATE coffee_abonements
|
// NEW: detect optional purchaser_user_id column
|
||||||
SET status = 'active',
|
const hasPurchaser = await this.hasColumn('purchaser_user_id');
|
||||||
pack_group = ?,
|
|
||||||
started_at = IFNULL(started_at, ?),
|
// Build SET clause dynamically
|
||||||
next_billing_at = ?,
|
const sets = [
|
||||||
billing_interval = ?,
|
`status = 'active'`,
|
||||||
interval_count = ?,
|
`pack_group = ?`,
|
||||||
price = ?,
|
`started_at = IFNULL(started_at, ?)`,
|
||||||
currency = ?,
|
`next_billing_at = ?`,
|
||||||
is_auto_renew = ?,
|
`billing_interval = ?`,
|
||||||
notes = ?,
|
`interval_count = ?`,
|
||||||
recipient_email = ?,
|
`price = ?`,
|
||||||
purchaser_user_id = IFNULL(purchaser_user_id, ?),
|
`currency = ?`,
|
||||||
pack_breakdown = ?, -- NEW
|
`is_auto_renew = ?`,
|
||||||
updated_at = NOW()
|
`notes = ?`,
|
||||||
WHERE id = ?`,
|
`recipient_email = ?`,
|
||||||
[
|
`pack_breakdown = ?`,
|
||||||
|
`updated_at = NOW()`,
|
||||||
|
];
|
||||||
|
const params = [
|
||||||
snapshot.pack_group || '',
|
snapshot.pack_group || '',
|
||||||
snapshot.started_at,
|
snapshot.started_at,
|
||||||
snapshot.next_billing_at,
|
snapshot.next_billing_at,
|
||||||
@ -246,11 +312,21 @@ class AbonemmentRepository {
|
|||||||
snapshot.is_auto_renew ? 1 : 0,
|
snapshot.is_auto_renew ? 1 : 0,
|
||||||
snapshot.notes || null,
|
snapshot.notes || null,
|
||||||
snapshot.recipient_email || null,
|
snapshot.recipient_email || null,
|
||||||
snapshot.purchaser_user_id ?? null,
|
|
||||||
snapshot.pack_breakdown ? JSON.stringify(snapshot.pack_breakdown) : null,
|
snapshot.pack_breakdown ? JSON.stringify(snapshot.pack_breakdown) : null,
|
||||||
id,
|
];
|
||||||
],
|
|
||||||
|
if (hasPurchaser) {
|
||||||
|
sets.splice(11, 0, `purchaser_user_id = IFNULL(purchaser_user_id, ?)`); // before pack_breakdown
|
||||||
|
params.splice(11, 0, snapshot.purchaser_user_id ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.query(
|
||||||
|
`UPDATE coffee_abonements
|
||||||
|
SET ${sets.join(', ')}
|
||||||
|
WHERE id = ?`,
|
||||||
|
[...params, id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const historyDetails = { ...(snapshot.details || {}), reused_existing: true, pack_group: snapshot.pack_group };
|
const historyDetails = { ...(snapshot.details || {}), reused_existing: true, pack_group: snapshot.pack_group };
|
||||||
if (snapshot.recipient) historyDetails.recipient = snapshot.recipient;
|
if (snapshot.recipient) historyDetails.recipient = snapshot.recipient;
|
||||||
await conn.query(
|
await conn.query(
|
||||||
|
|||||||
184
repositories/invoice/InvoiceRepository.js
Normal file
184
repositories/invoice/InvoiceRepository.js
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
const pool = require('../../database/database');
|
||||||
|
const Invoice = require('../../models/Invoice');
|
||||||
|
|
||||||
|
function genInvoiceNumber() {
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const m = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(now.getDate()).padStart(2, '0');
|
||||||
|
const rand = Math.floor(Math.random() * 1e6).toString().padStart(6, '0');
|
||||||
|
return `INV-${y}${m}${d}-${rand}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvoiceRepository {
|
||||||
|
async createInvoiceWithItems({
|
||||||
|
source_type,
|
||||||
|
source_id,
|
||||||
|
user_id,
|
||||||
|
buyer_name,
|
||||||
|
buyer_email,
|
||||||
|
buyer_street,
|
||||||
|
buyer_postal_code,
|
||||||
|
buyer_city,
|
||||||
|
buyer_country,
|
||||||
|
currency,
|
||||||
|
items = [],
|
||||||
|
status = 'draft',
|
||||||
|
issued_at = null,
|
||||||
|
due_at = null,
|
||||||
|
context = null,
|
||||||
|
vat_rate = null, // NEW: default VAT for invoice and lines
|
||||||
|
}) {
|
||||||
|
const conn = await pool.getConnection();
|
||||||
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
const invoice_number = genInvoiceNumber();
|
||||||
|
|
||||||
|
// compute totals
|
||||||
|
let total_net = 0;
|
||||||
|
let total_tax = 0;
|
||||||
|
let total_gross = 0;
|
||||||
|
for (const it of items) {
|
||||||
|
const qty = Number(it.quantity || 1);
|
||||||
|
const unit = Number(it.unit_price || 0);
|
||||||
|
const rate = it.tax_rate != null ? Number(it.tax_rate) : (vat_rate != null ? Number(vat_rate) : null); // CHANGED
|
||||||
|
const line_net = qty * unit;
|
||||||
|
const line_tax = rate != null ? +(line_net * (rate / 100)).toFixed(2) : 0;
|
||||||
|
const line_gross = +(line_net + line_tax).toFixed(2);
|
||||||
|
total_net += line_net;
|
||||||
|
total_tax += line_tax;
|
||||||
|
total_gross += line_gross;
|
||||||
|
}
|
||||||
|
total_net = +total_net.toFixed(2);
|
||||||
|
total_tax = +total_tax.toFixed(2);
|
||||||
|
total_gross = +total_gross.toFixed(2);
|
||||||
|
|
||||||
|
const [res] = await conn.query(
|
||||||
|
`INSERT INTO invoices
|
||||||
|
(invoice_number, user_id, source_type, source_id, buyer_name, buyer_email, buyer_street, buyer_postal_code, buyer_city, buyer_country,
|
||||||
|
currency, total_net, total_tax, total_gross, vat_rate, status, issued_at, due_at, pdf_storage_key, context, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, NOW(), NOW())`,
|
||||||
|
[
|
||||||
|
invoice_number,
|
||||||
|
user_id || null,
|
||||||
|
source_type,
|
||||||
|
source_id,
|
||||||
|
buyer_name || null,
|
||||||
|
buyer_email || null,
|
||||||
|
buyer_street || null,
|
||||||
|
buyer_postal_code || null,
|
||||||
|
buyer_city || null,
|
||||||
|
buyer_country || null,
|
||||||
|
currency,
|
||||||
|
total_net,
|
||||||
|
total_tax,
|
||||||
|
total_gross,
|
||||||
|
vat_rate != null ? Number(vat_rate) : null, // CHANGED
|
||||||
|
status,
|
||||||
|
issued_at || null,
|
||||||
|
due_at || null,
|
||||||
|
context ? JSON.stringify(context) : null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const invoiceId = res.insertId;
|
||||||
|
|
||||||
|
for (const it of items) {
|
||||||
|
const qty = Number(it.quantity || 1);
|
||||||
|
const unit = Number(it.unit_price || 0);
|
||||||
|
const rate = it.tax_rate != null ? Number(it.tax_rate) : (vat_rate != null ? Number(vat_rate) : null); // CHANGED
|
||||||
|
const line_net = +(qty * unit).toFixed(2);
|
||||||
|
const line_tax = rate != null ? +(line_net * (rate / 100)).toFixed(2) : 0;
|
||||||
|
const line_gross = +(line_net + line_tax).toFixed(2);
|
||||||
|
|
||||||
|
await conn.query(
|
||||||
|
`INSERT INTO invoice_items
|
||||||
|
(invoice_id, product_id, sku, description, quantity, unit_price, tax_rate, line_net, line_tax, line_gross, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||||
|
[
|
||||||
|
invoiceId,
|
||||||
|
it.product_id || null,
|
||||||
|
it.sku || null,
|
||||||
|
it.description || 'Subscription',
|
||||||
|
qty,
|
||||||
|
unit,
|
||||||
|
rate, // CHANGED
|
||||||
|
line_net,
|
||||||
|
line_tax,
|
||||||
|
line_gross,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.commit();
|
||||||
|
return this.getById(invoiceId);
|
||||||
|
} catch (e) {
|
||||||
|
await conn.rollback();
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
conn.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markPaid(invoiceId, { payment_method, transaction_id, amount, paid_at, details }) {
|
||||||
|
const conn = await pool.getConnection();
|
||||||
|
try {
|
||||||
|
await conn.beginTransaction();
|
||||||
|
await conn.query(`UPDATE invoices SET status='paid', updated_at=NOW() WHERE id = ?`, [invoiceId]);
|
||||||
|
await conn.query(
|
||||||
|
`INSERT INTO invoice_payments
|
||||||
|
(invoice_id, payment_method, transaction_id, amount, paid_at, status, details, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'succeeded', ?, NOW())`,
|
||||||
|
[invoiceId, payment_method || 'manual', transaction_id || null, amount || null, paid_at || new Date(), details ? JSON.stringify(details) : null],
|
||||||
|
);
|
||||||
|
await conn.commit();
|
||||||
|
return this.getById(invoiceId);
|
||||||
|
} catch (e) {
|
||||||
|
await conn.rollback();
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
conn.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(id) {
|
||||||
|
const [rows] = await pool.query(`SELECT * FROM invoices WHERE id = ?`, [id]);
|
||||||
|
return rows[0] ? new Invoice(rows[0]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listByUser(userId, { status, limit = 50, offset = 0 } = {}) {
|
||||||
|
const params = [userId];
|
||||||
|
let sql = `SELECT * FROM invoices WHERE user_id = ?`;
|
||||||
|
if (status) {
|
||||||
|
sql += ` AND status = ?`;
|
||||||
|
params.push(status);
|
||||||
|
}
|
||||||
|
sql += ` ORDER BY issued_at DESC, created_at DESC LIMIT ? OFFSET ?`;
|
||||||
|
params.push(Number(limit), Number(offset));
|
||||||
|
const [rows] = await pool.query(sql, params);
|
||||||
|
return rows.map((r) => new Invoice(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
async listAll({ status, limit = 200, offset = 0 } = {}) {
|
||||||
|
const params = [];
|
||||||
|
let sql = `SELECT * FROM invoices`;
|
||||||
|
if (status) {
|
||||||
|
sql += ` WHERE status = ?`;
|
||||||
|
params.push(status);
|
||||||
|
}
|
||||||
|
sql += ` ORDER BY issued_at DESC, created_at DESC LIMIT ? OFFSET ?`;
|
||||||
|
params.push(Number(limit), Number(offset));
|
||||||
|
const [rows] = await pool.query(sql, params);
|
||||||
|
return rows.map((r) => new Invoice(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByAbonement(abonementId) {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT * FROM invoices WHERE source_type='subscription' AND source_id = ? ORDER BY issued_at DESC, id DESC`,
|
||||||
|
[abonementId],
|
||||||
|
);
|
||||||
|
return rows.map((r) => new Invoice(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = InvoiceRepository;
|
||||||
@ -563,14 +563,23 @@ async function addUserToMatrix({
|
|||||||
err.status = 400;
|
err.status = 400;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
// Depth-policy check: placing under non-root referrer must not exceed max depth
|
||||||
|
if (maxDepthPolicy != null) {
|
||||||
if (!parentId) {
|
const parentDepthFromRoot = Number(inMatrixRows[0].depth || 0);
|
||||||
// No referral parent found
|
if (parentDepthFromRoot + 1 > maxDepthPolicy) {
|
||||||
fallback_reason = 'referrer_not_in_matrix';
|
if (forceParentFallback) {
|
||||||
|
fallback_reason = 'referrer_depth_limit';
|
||||||
parentId = rid;
|
parentId = rid;
|
||||||
rogueFlag = true;
|
rogueFlag = true;
|
||||||
|
} else {
|
||||||
|
const err = new Error('Referrer depth limit would be exceeded');
|
||||||
|
err.status = 400;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duplicate check (already in matrix)
|
// Duplicate check (already in matrix)
|
||||||
@ -661,7 +670,8 @@ async function addUserToMatrix({
|
|||||||
if (!assignPos) {
|
if (!assignPos) {
|
||||||
// no slot in subtree -> decide fallback
|
// no slot in subtree -> decide fallback
|
||||||
if (forceParentFallback) {
|
if (forceParentFallback) {
|
||||||
fallback_reason = 'referrer_full';
|
// If maxDepthPolicy prevented placement anywhere under referrer subtree, mark depth-limit reason when applicable
|
||||||
|
fallback_reason = (maxDepthPolicy != null) ? 'referrer_depth_limit' : 'referrer_full';
|
||||||
parentId = rid;
|
parentId = rid;
|
||||||
rogueFlag = true;
|
rogueFlag = true;
|
||||||
// root sequential
|
// root sequential
|
||||||
@ -672,7 +682,7 @@ async function addUserToMatrix({
|
|||||||
const maxPos = Number(rootPosRows[0]?.maxPos || 0);
|
const maxPos = Number(rootPosRows[0]?.maxPos || 0);
|
||||||
assignPos = maxPos + 1;
|
assignPos = maxPos + 1;
|
||||||
} else {
|
} else {
|
||||||
const err = new Error('Parent subtree is full (5-ary); no available position');
|
const err = new Error('Parent subtree is full (5-ary) or depth limit reached; no available position');
|
||||||
err.status = 409;
|
err.status = 409;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,27 +7,35 @@ class PoolRepository {
|
|||||||
|
|
||||||
async create({ pool_name, description = null, price = 0.00, pool_type = 'other', is_active = true, created_by = null }) {
|
async create({ pool_name, description = null, price = 0.00, pool_type = 'other', is_active = true, created_by = null }) {
|
||||||
const conn = this.uow.connection;
|
const conn = this.uow.connection;
|
||||||
const [res] = await conn.execute(
|
try {
|
||||||
`INSERT INTO pools (pool_name, description, price, pool_type, is_active, created_by)
|
console.info('PoolRepository.create:start', { pool_name, pool_type, is_active, price, created_by });
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
const sql = `INSERT INTO pools (pool_name, description, price, pool_type, is_active, created_by)
|
||||||
[pool_name, description, price, pool_type, is_active, created_by]
|
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
);
|
const params = [pool_name, description, price, pool_type, is_active, created_by];
|
||||||
|
const [res] = await conn.execute(sql, params);
|
||||||
|
console.info('PoolRepository.create:success', { insertId: res?.insertId });
|
||||||
return new Pool({ id: res.insertId, pool_name, description, price, pool_type, is_active, created_by });
|
return new Pool({ id: res.insertId, pool_name, description, price, pool_type, is_active, created_by });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('PoolRepository.create:error', { code: err?.code, errno: err?.errno, sqlMessage: err?.sqlMessage, message: err?.message });
|
||||||
|
const e = new Error('Failed to create pool');
|
||||||
|
e.status = 500;
|
||||||
|
e.cause = err;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll() {
|
async findAll() {
|
||||||
const conn = this.uow.connection; // switched to connection
|
const conn = this.uow.connection; // switched to connection
|
||||||
try {
|
try {
|
||||||
console.debug('[PoolRepository.findAll] querying pools');
|
console.info('PoolRepository.findAll:start');
|
||||||
const [rows] = await conn.execute(
|
const sql = `SELECT id, pool_name, description, price, pool_type, is_active, created_by, updated_by, created_at, updated_at
|
||||||
`SELECT id, pool_name, description, price, pool_type, is_active, created_by, updated_by, created_at, updated_at
|
|
||||||
FROM pools
|
FROM pools
|
||||||
ORDER BY created_at DESC`
|
ORDER BY created_at DESC`;
|
||||||
);
|
const [rows] = await conn.execute(sql);
|
||||||
console.debug('[PoolRepository.findAll] rows fetched', { count: rows.length });
|
console.info('PoolRepository.findAll:success', { count: rows.length });
|
||||||
return rows.map(r => new Pool(r));
|
return rows.map(r => new Pool(r));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[PoolRepository.findAll] query failed', err);
|
console.error('PoolRepository.findAll:error', { code: err?.code, errno: err?.errno, sqlMessage: err?.sqlMessage, message: err?.message });
|
||||||
// Surface a consistent error up the stack
|
// Surface a consistent error up the stack
|
||||||
const e = new Error('Failed to fetch pools');
|
const e = new Error('Failed to fetch pools');
|
||||||
e.status = 500;
|
e.status = 500;
|
||||||
@ -39,8 +47,11 @@ class PoolRepository {
|
|||||||
// Update is_active flag (replaces old state transitions)
|
// Update is_active flag (replaces old state transitions)
|
||||||
async updateActive(id, is_active, updated_by = null) {
|
async updateActive(id, is_active, updated_by = null) {
|
||||||
const conn = this.uow.connection;
|
const conn = this.uow.connection;
|
||||||
|
try {
|
||||||
|
console.info('PoolRepository.updateActive:start', { id, is_active, updated_by });
|
||||||
const [rows] = await conn.execute(`SELECT id FROM pools WHERE id = ?`, [id]);
|
const [rows] = await conn.execute(`SELECT id FROM pools WHERE id = ?`, [id]);
|
||||||
if (!rows || rows.length === 0) {
|
if (!rows || rows.length === 0) {
|
||||||
|
console.warn('PoolRepository.updateActive:not_found', { id });
|
||||||
const err = new Error('Pool not found');
|
const err = new Error('Pool not found');
|
||||||
err.status = 404;
|
err.status = 404;
|
||||||
throw err;
|
throw err;
|
||||||
@ -53,7 +64,18 @@ class PoolRepository {
|
|||||||
`SELECT id, pool_name, description, price, pool_type, is_active, created_by, updated_by, created_at, updated_at FROM pools WHERE id = ?`,
|
`SELECT id, pool_name, description, price, pool_type, is_active, created_by, updated_by, created_at, updated_at FROM pools WHERE id = ?`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
console.info('PoolRepository.updateActive:success', { id, is_active });
|
||||||
return new Pool(updated[0]);
|
return new Pool(updated[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('PoolRepository.updateActive:error', { id, is_active, code: err?.code, errno: err?.errno, sqlMessage: err?.sqlMessage, message: err?.message });
|
||||||
|
if (!err.status) {
|
||||||
|
const e = new Error('Failed to update pool active state');
|
||||||
|
e.status = 500;
|
||||||
|
e.cause = err;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const TaxController = require('../controller/tax/taxController');
|
|||||||
const AffiliateController = require('../controller/affiliate/AffiliateController');
|
const AffiliateController = require('../controller/affiliate/AffiliateController');
|
||||||
const AbonemmentController = require('../controller/abonemments/AbonemmentController');
|
const AbonemmentController = require('../controller/abonemments/AbonemmentController');
|
||||||
const NewsController = require('../controller/news/NewsController');
|
const NewsController = require('../controller/news/NewsController');
|
||||||
|
const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW
|
||||||
|
|
||||||
// small helpers copied from original files
|
// small helpers copied from original files
|
||||||
function adminOnly(req, res, next) {
|
function adminOnly(req, res, next) {
|
||||||
@ -160,5 +161,9 @@ router.get('/admin/abonements', authMiddleware, adminOnly, AbonemmentController.
|
|||||||
router.get('/admin/news', authMiddleware, adminOnly, NewsController.list);
|
router.get('/admin/news', authMiddleware, adminOnly, NewsController.list);
|
||||||
router.get('/news/active', NewsController.listActive);
|
router.get('/news/active', NewsController.listActive);
|
||||||
|
|
||||||
|
// NEW: Invoice GETs
|
||||||
|
router.get('/invoices/mine', authMiddleware, InvoiceController.listMine);
|
||||||
|
router.get('/admin/invoices', authMiddleware, adminOnly, InvoiceController.adminList);
|
||||||
|
|
||||||
// export
|
// export
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@ -27,6 +27,7 @@ const TaxController = require('../controller/tax/taxController');
|
|||||||
const AffiliateController = require('../controller/affiliate/AffiliateController');
|
const AffiliateController = require('../controller/affiliate/AffiliateController');
|
||||||
const AbonemmentController = require('../controller/abonemments/AbonemmentController');
|
const AbonemmentController = require('../controller/abonemments/AbonemmentController');
|
||||||
const NewsController = require('../controller/news/NewsController');
|
const NewsController = require('../controller/news/NewsController');
|
||||||
|
const InvoiceController = require('../controller/invoice/InvoiceController'); // NEW
|
||||||
|
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
@ -169,6 +170,9 @@ router.post('/admin/abonements/:id/renew', authMiddleware, adminOnly, Abonemment
|
|||||||
// CHANGED: ensure req.user has id/email from body for this route
|
// CHANGED: ensure req.user has id/email from body for this route
|
||||||
router.post('/abonements/referred', authMiddleware, ensureUserFromBody, AbonemmentController.getReferredSubscriptions);
|
router.post('/abonements/referred', authMiddleware, ensureUserFromBody, AbonemmentController.getReferredSubscriptions);
|
||||||
|
|
||||||
|
// NEW: Invoice POSTs
|
||||||
|
router.post('/invoices/:id/pay', authMiddleware, adminOnly, InvoiceController.pay);
|
||||||
|
|
||||||
// Existing registration handlers (keep)
|
// Existing registration handlers (keep)
|
||||||
router.post('/register/personal', (req, res) => {
|
router.post('/register/personal', (req, res) => {
|
||||||
console.log('🔗 POST /register/personal route accessed');
|
console.log('🔗 POST /register/personal route accessed');
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
const pool = require('../../database/database');
|
const pool = require('../../database/database');
|
||||||
const AbonemmentRepository = require('../../repositories/abonemments/AbonemmentRepository');
|
const AbonemmentRepository = require('../../repositories/abonemments/AbonemmentRepository');
|
||||||
|
const InvoiceService = require('../invoice/InvoiceService'); // NEW
|
||||||
|
|
||||||
class AbonemmentService {
|
class AbonemmentService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.repo = new AbonemmentRepository();
|
this.repo = new AbonemmentRepository();
|
||||||
|
this.invoiceService = new InvoiceService(); // NEW
|
||||||
}
|
}
|
||||||
|
|
||||||
isAdmin(user) {
|
isAdmin(user) {
|
||||||
@ -51,6 +53,7 @@ class AbonemmentService {
|
|||||||
referredBy, // NEW: referred_by field
|
referredBy, // NEW: referred_by field
|
||||||
}) {
|
}) {
|
||||||
console.log('[SUBSCRIBE ORDER] Start processing subscription order');
|
console.log('[SUBSCRIBE ORDER] Start processing subscription order');
|
||||||
|
console.log('[SUBSCRIBE ORDER] Actor user:', actorUser ? { id: actorUser.id, role: actorUser.role, email: actorUser.email } : actorUser);
|
||||||
console.log('[SUBSCRIBE ORDER] Payload:', {
|
console.log('[SUBSCRIBE ORDER] Payload:', {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
@ -116,9 +119,53 @@ class AbonemmentService {
|
|||||||
country,
|
country,
|
||||||
frequency,
|
frequency,
|
||||||
referred_by: referredBy || null, // Pass referred_by to snapshot
|
referred_by: referredBy || null, // Pass referred_by to snapshot
|
||||||
|
user_id: actorUser?.id ?? null, // NEW: set owner (purchaser acts as owner here)
|
||||||
|
purchaser_user_id: actorUser?.id ?? null, // NEW: also store purchaser
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.repo.createAbonement(referredBy, snapshot); // Pass referredBy to repository
|
console.log('[SUBSCRIBE ORDER] Snapshot user linking:', {
|
||||||
|
actor_user_id: snapshot.actor_user_id,
|
||||||
|
user_id: snapshot.user_id,
|
||||||
|
purchaser_user_id: snapshot.purchaser_user_id,
|
||||||
|
referred_by: snapshot.referred_by,
|
||||||
|
});
|
||||||
|
|
||||||
|
const abonement = await this.repo.createAbonement(referredBy, snapshot); // Pass referredBy to repository
|
||||||
|
console.log('[SUBSCRIBE ORDER] Created abonement:', {
|
||||||
|
id: abonement?.id,
|
||||||
|
user_id: abonement?.user_id,
|
||||||
|
purchaser_user_id: abonement?.purchaser_user_id,
|
||||||
|
status: abonement?.status,
|
||||||
|
pack_group: abonement?.pack_group,
|
||||||
|
});
|
||||||
|
|
||||||
|
// NEW: issue invoice for first period and append history
|
||||||
|
try {
|
||||||
|
const invoice = await this.invoiceService.issueForAbonement(
|
||||||
|
abonement,
|
||||||
|
snapshot.started_at,
|
||||||
|
snapshot.next_billing_at,
|
||||||
|
{ actorUserId: actorUser?.id || null }
|
||||||
|
);
|
||||||
|
console.log('[SUBSCRIBE ORDER] Issued invoice:', {
|
||||||
|
id: invoice?.id,
|
||||||
|
user_id: invoice?.user_id,
|
||||||
|
total_gross: invoice?.total_gross,
|
||||||
|
vat_rate: invoice?.vat_rate,
|
||||||
|
});
|
||||||
|
await this.repo.appendHistory(
|
||||||
|
abonement.id,
|
||||||
|
'invoice_issued',
|
||||||
|
actorUser?.id || null,
|
||||||
|
{ pack_group: abonement.pack_group, invoiceId: invoice.id },
|
||||||
|
new Date()
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SUBSCRIBE ORDER] Invoice issue failed:', e);
|
||||||
|
// intentionally not throwing to avoid blocking subscription; adjust if you want transactional consistency
|
||||||
|
}
|
||||||
|
|
||||||
|
return abonement;
|
||||||
}
|
}
|
||||||
|
|
||||||
async subscribe({
|
async subscribe({
|
||||||
@ -135,7 +182,8 @@ class AbonemmentService {
|
|||||||
referredBy, // NEW: referred_by field
|
referredBy, // NEW: referred_by field
|
||||||
}) {
|
}) {
|
||||||
console.log('[SUBSCRIBE] Start processing single subscription'); // NEW
|
console.log('[SUBSCRIBE] Start processing single subscription'); // NEW
|
||||||
console.log('[SUBSCRIBE] Recipient email:', recipientEmail); // NEW
|
console.log('[SUBSCRIBE] Actor user:', actorUser ? { id: actorUser.id, role: actorUser.role, email: actorUser.email } : actorUser);
|
||||||
|
console.log('[SUBSCRIBE] Incoming userId:', userId, 'targetUserId:', targetUserId); // NEW
|
||||||
|
|
||||||
const normalizedRecipientEmail = this.normalizeEmail(recipientEmail);
|
const normalizedRecipientEmail = this.normalizeEmail(recipientEmail);
|
||||||
console.log('[SUBSCRIBE] Normalized recipient email:', normalizedRecipientEmail); // NEW
|
console.log('[SUBSCRIBE] Normalized recipient email:', normalizedRecipientEmail); // NEW
|
||||||
@ -150,6 +198,7 @@ class AbonemmentService {
|
|||||||
const isForMe = !targetUserId && !hasRecipientFields;
|
const isForMe = !targetUserId && !hasRecipientFields;
|
||||||
const ownerUserId = targetUserId ?? (hasRecipientFields ? null : safeUserId);
|
const ownerUserId = targetUserId ?? (hasRecipientFields ? null : safeUserId);
|
||||||
const purchaserUserId = isForMe ? null : actorUser?.id || null;
|
const purchaserUserId = isForMe ? null : actorUser?.id || null;
|
||||||
|
console.log('[SUBSCRIBE] Resolved ownership:', { isForMe, ownerUserId, purchaserUserId }); // NEW
|
||||||
|
|
||||||
const recipientMeta = targetUserId
|
const recipientMeta = targetUserId
|
||||||
? { target_user_id: targetUserId }
|
? { target_user_id: targetUserId }
|
||||||
@ -186,15 +235,57 @@ class AbonemmentService {
|
|||||||
recipient_email: normalizedRecipientEmail || null, // CHANGED
|
recipient_email: normalizedRecipientEmail || null, // CHANGED
|
||||||
details,
|
details,
|
||||||
recipient: recipientMeta || undefined,
|
recipient: recipientMeta || undefined,
|
||||||
purchaser_user_id: purchaserUserId, // NEW
|
purchaser_user_id, // NEW
|
||||||
|
user_id: ownerUserId ?? null, // NEW: persist owner
|
||||||
referred_by: referredBy || null, // Pass referred_by to snapshot
|
referred_by: referredBy || null, // Pass referred_by to snapshot
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[SUBSCRIBE] Snapshot user linking:', {
|
||||||
|
actor_user_id: snapshot.actor_user_id,
|
||||||
|
user_id: snapshot.user_id,
|
||||||
|
purchaser_user_id: snapshot.purchaser_user_id,
|
||||||
|
referred_by: snapshot.referred_by,
|
||||||
|
});
|
||||||
|
|
||||||
const existing = await this.repo.findActiveOrPausedByUserAndProduct(ownerUserId ?? null, canonicalPackGroup);
|
const existing = await this.repo.findActiveOrPausedByUserAndProduct(ownerUserId ?? null, canonicalPackGroup);
|
||||||
|
console.log('[SUBSCRIBE] Existing abonement candidate:', existing ? { id: existing.id, user_id: existing.user_id } : null); // NEW
|
||||||
const abonement = existing
|
const abonement = existing
|
||||||
? await this.repo.updateExistingAbonementForSubscribe(existing.id, snapshot)
|
? await this.repo.updateExistingAbonementForSubscribe(existing.id, snapshot)
|
||||||
: await this.repo.createAbonement(ownerUserId ?? null, snapshot, referredBy); // Pass referredBy to repository
|
: await this.repo.createAbonement(ownerUserId ?? null, snapshot, referredBy); // Pass referredBy to repository
|
||||||
|
|
||||||
|
console.log('[SUBSCRIBE] Upserted abonement:', {
|
||||||
|
id: abonement?.id,
|
||||||
|
user_id: abonement?.user_id,
|
||||||
|
purchaser_user_id: abonement?.purchaser_user_id,
|
||||||
|
status: abonement?.status,
|
||||||
|
pack_group: abonement?.pack_group,
|
||||||
|
});
|
||||||
|
|
||||||
|
// NEW: issue invoice for first period and append history
|
||||||
|
try {
|
||||||
|
const invoice = await this.invoiceService.issueForAbonement(
|
||||||
|
abonement,
|
||||||
|
snapshot.started_at,
|
||||||
|
snapshot.next_billing_at,
|
||||||
|
{ actorUserId: actorUser?.id || null }
|
||||||
|
);
|
||||||
|
console.log('[SUBSCRIBE] Issued invoice:', {
|
||||||
|
id: invoice?.id,
|
||||||
|
user_id: invoice?.user_id,
|
||||||
|
total_gross: invoice?.total_gross,
|
||||||
|
vat_rate: invoice?.vat_rate,
|
||||||
|
});
|
||||||
|
await this.repo.appendHistory(
|
||||||
|
abonement.id,
|
||||||
|
'invoice_issued',
|
||||||
|
actorUser?.id || null,
|
||||||
|
{ pack_group: abonement.pack_group, invoiceId: invoice.id },
|
||||||
|
new Date()
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SUBSCRIBE] Invoice issue failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[SUBSCRIBE] Single subscription completed successfully'); // NEW
|
console.log('[SUBSCRIBE] Single subscription completed successfully'); // NEW
|
||||||
return abonement;
|
return abonement;
|
||||||
}
|
}
|
||||||
@ -251,11 +342,32 @@ class AbonemmentService {
|
|||||||
if (!abon) throw new Error('Not found');
|
if (!abon) throw new Error('Not found');
|
||||||
if (!abon.isActive) throw new Error('Only active abonements can be renewed');
|
if (!abon.isActive) throw new Error('Only active abonements can be renewed');
|
||||||
const next = this.addInterval(new Date(abon.next_billing_at || new Date()), abon.billing_interval, abon.interval_count);
|
const next = this.addInterval(new Date(abon.next_billing_at || new Date()), abon.billing_interval, abon.interval_count);
|
||||||
return this.repo.transitionBilling(abonementId, next, {
|
const renewed = await this.repo.transitionBilling(abonementId, next, {
|
||||||
event_type: 'renewed',
|
event_type: 'renewed',
|
||||||
actor_user_id: actorUser?.id || null,
|
actor_user_id: actorUser?.id || null,
|
||||||
details: { pack_group: abon.pack_group, ...(invoiceId ? { invoiceId } : {}) },
|
details: { pack_group: abon.pack_group, ...(invoiceId ? { invoiceId } : {}) },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NEW: issue invoice for next cycle and attach to history
|
||||||
|
try {
|
||||||
|
const invoice = await this.invoiceService.issueForAbonement(
|
||||||
|
renewed,
|
||||||
|
new Date(abon.next_billing_at || new Date()),
|
||||||
|
next,
|
||||||
|
{ actorUserId: actorUser?.id || null }
|
||||||
|
);
|
||||||
|
await this.repo.appendHistory(
|
||||||
|
abonementId,
|
||||||
|
'invoice_issued',
|
||||||
|
actorUser?.id || null,
|
||||||
|
{ pack_group: renewed.pack_group, invoiceId: invoice.id },
|
||||||
|
new Date()
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[RENEW] Invoice issue failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renewed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMyAbonements({ userId }) {
|
async getMyAbonements({ userId }) {
|
||||||
|
|||||||
140
services/invoice/InvoiceService.js
Normal file
140
services/invoice/InvoiceService.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
const InvoiceRepository = require('../../repositories/invoice/InvoiceRepository');
|
||||||
|
const UnitOfWork = require('../../database/UnitOfWork'); // NEW
|
||||||
|
const TaxRepository = require('../../repositories/tax/taxRepository'); // NEW
|
||||||
|
|
||||||
|
class InvoiceService {
|
||||||
|
constructor() {
|
||||||
|
this.repo = new InvoiceRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: resolve current standard VAT rate for a buyer country code
|
||||||
|
async resolveVatRateForCountry(countryCode) {
|
||||||
|
if (!countryCode) return null;
|
||||||
|
const uow = new UnitOfWork();
|
||||||
|
await uow.start();
|
||||||
|
const taxRepo = new TaxRepository(uow);
|
||||||
|
try {
|
||||||
|
const country = await taxRepo.getCountryByCode(String(countryCode).toUpperCase());
|
||||||
|
if (!country) {
|
||||||
|
await uow.commit();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// get current vat row for this country
|
||||||
|
const [rows] = await taxRepo.conn.query(
|
||||||
|
`SELECT standard_rate FROM vat_rates WHERE country_id = ? AND effective_to IS NULL LIMIT 1`,
|
||||||
|
[country.id]
|
||||||
|
);
|
||||||
|
await uow.commit();
|
||||||
|
const rate = rows?.[0]?.standard_rate;
|
||||||
|
return rate == null ? null : Number(rate);
|
||||||
|
} catch (e) {
|
||||||
|
await uow.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue invoice for a subscription period, with items from pack_breakdown
|
||||||
|
async issueForAbonement(abonement, periodStart, periodEnd, { actorUserId } = {}) {
|
||||||
|
console.log('[INVOICE ISSUE] Inputs:', {
|
||||||
|
abonement_id: abonement?.id,
|
||||||
|
abonement_user_id: abonement?.user_id,
|
||||||
|
abonement_purchaser_user_id: abonement?.purchaser_user_id,
|
||||||
|
actorUserId,
|
||||||
|
periodStart,
|
||||||
|
periodEnd,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buyerName = [abonement.first_name, abonement.last_name].filter(Boolean).join(' ') || null;
|
||||||
|
const buyerEmail = abonement.email || null;
|
||||||
|
const addr = {
|
||||||
|
street: abonement.street || null,
|
||||||
|
postal_code: abonement.postal_code || null,
|
||||||
|
city: abonement.city || null,
|
||||||
|
country: abonement.country || null,
|
||||||
|
};
|
||||||
|
const currency = abonement.currency || 'EUR';
|
||||||
|
|
||||||
|
// NEW: resolve invoice vat_rate (standard) from buyer country
|
||||||
|
const vat_rate = await this.resolveVatRateForCountry(addr.country);
|
||||||
|
|
||||||
|
const breakdown = Array.isArray(abonement.pack_breakdown) ? abonement.pack_breakdown : [];
|
||||||
|
const items = breakdown.length
|
||||||
|
? breakdown.map((b) => ({
|
||||||
|
product_id: Number(b.coffee_table_id) || null,
|
||||||
|
sku: `COFFEE-${b.coffee_table_id || 'N/A'}`,
|
||||||
|
description: `Coffee subscription: ${b.coffee_table_id}`,
|
||||||
|
quantity: Number(b.packs || 1),
|
||||||
|
unit_price: Number(b.price_per_pack || 0),
|
||||||
|
tax_rate: b.tax_rate != null ? Number(b.tax_rate) : vat_rate, // CHANGED: default to invoice vat_rate
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
product_id: null,
|
||||||
|
sku: 'SUBSCRIPTION',
|
||||||
|
description: `Subscription ${abonement.pack_group || ''}`,
|
||||||
|
quantity: 1,
|
||||||
|
unit_price: Number(abonement.price || 0),
|
||||||
|
tax_rate: vat_rate, // CHANGED
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
source: 'abonement',
|
||||||
|
pack_group: abonement.pack_group || null,
|
||||||
|
period_start: periodStart,
|
||||||
|
period_end: periodEnd,
|
||||||
|
referred_by: abonement.referred_by || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// CHANGED: prioritize token user id for invoice ownership
|
||||||
|
const userIdForInvoice =
|
||||||
|
actorUserId ?? abonement.user_id ?? abonement.purchaser_user_id ?? null;
|
||||||
|
|
||||||
|
console.log('[INVOICE ISSUE] Resolved user_id for invoice:', userIdForInvoice);
|
||||||
|
|
||||||
|
const invoice = await this.repo.createInvoiceWithItems({
|
||||||
|
source_type: 'subscription',
|
||||||
|
source_id: abonement.id,
|
||||||
|
user_id: userIdForInvoice,
|
||||||
|
buyer_name: buyerName,
|
||||||
|
buyer_email: buyerEmail,
|
||||||
|
buyer_street: addr.street,
|
||||||
|
buyer_postal_code: addr.postal_code,
|
||||||
|
buyer_city: addr.city,
|
||||||
|
buyer_country: addr.country,
|
||||||
|
currency,
|
||||||
|
items,
|
||||||
|
status: 'issued',
|
||||||
|
issued_at: new Date(),
|
||||||
|
due_at: periodEnd,
|
||||||
|
context,
|
||||||
|
vat_rate, // NEW: persist on invoice
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[INVOICE ISSUE] Created invoice:', {
|
||||||
|
id: invoice?.id,
|
||||||
|
user_id: invoice?.user_id,
|
||||||
|
source_type: invoice?.source_type,
|
||||||
|
source_id: invoice?.source_id,
|
||||||
|
total_net: invoice?.total_net,
|
||||||
|
total_tax: invoice?.total_tax,
|
||||||
|
total_gross: invoice?.total_gross,
|
||||||
|
});
|
||||||
|
|
||||||
|
return invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
async markPaid(invoiceId, { payment_method, transaction_id, amount, paid_at = new Date(), details } = {}) {
|
||||||
|
return this.repo.markPaid(invoiceId, { payment_method, transaction_id, amount, paid_at, details });
|
||||||
|
}
|
||||||
|
|
||||||
|
async listMine(userId, { status, limit = 50, offset = 0 } = {}) {
|
||||||
|
return this.repo.listByUser(userId, { status, limit, offset });
|
||||||
|
}
|
||||||
|
|
||||||
|
async adminList({ status, limit = 200, offset = 0 } = {}) {
|
||||||
|
return this.repo.listAll({ status, limit, offset });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = InvoiceService;
|
||||||
Loading…
Reference in New Issue
Block a user