feat: enhance subscription model with additional contact and invoice fields
hawi i cant
This commit is contained in:
parent
46a081ae8f
commit
19847eefb4
@ -25,7 +25,6 @@ module.exports = {
|
|||||||
isForSelf: req.body.is_for_self,
|
isForSelf: req.body.is_for_self,
|
||||||
recipientName: req.body.recipient_name,
|
recipientName: req.body.recipient_name,
|
||||||
recipientEmail: req.body.recipient_email,
|
recipientEmail: req.body.recipient_email,
|
||||||
recipientNotes: req.body.recipient_notes,
|
|
||||||
firstName: req.body.firstName,
|
firstName: req.body.firstName,
|
||||||
lastName: req.body.lastName,
|
lastName: req.body.lastName,
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
@ -34,8 +33,20 @@ module.exports = {
|
|||||||
city: req.body.city,
|
city: req.body.city,
|
||||||
country: req.body.country,
|
country: req.body.country,
|
||||||
frequency: req.body.frequency,
|
frequency: req.body.frequency,
|
||||||
startDate: req.body.startDate,
|
phone: req.body.phone,
|
||||||
|
recipientContractName: req.body.recipientContractName,
|
||||||
|
recipientAddress: req.body.recipientAddress,
|
||||||
|
paymentMethod: req.body.paymentMethod,
|
||||||
|
invoiceByEmail: req.body.invoiceByEmail,
|
||||||
|
invoiceSameAsShipping: req.body.invoiceSameAsShipping,
|
||||||
|
invoiceFullName: req.body.invoiceFullName,
|
||||||
|
invoiceStreet: req.body.invoiceStreet,
|
||||||
|
invoicePostalCode: req.body.invoicePostalCode,
|
||||||
|
invoiceCity: req.body.invoiceCity,
|
||||||
|
invoicePhone: req.body.invoicePhone,
|
||||||
|
invoiceEmail: req.body.invoiceEmail,
|
||||||
contractNumber: req.body.contractNumber || req.body.contract_number,
|
contractNumber: req.body.contractNumber || req.body.contract_number,
|
||||||
|
signingCity: req.body.signingCity || req.body.signing_city || '',
|
||||||
signatureDataUrl: req.body.signatureDataUrl || req.body.signature_data_url,
|
signatureDataUrl: req.body.signatureDataUrl || req.body.signature_data_url,
|
||||||
actorUser, // normalized to include id
|
actorUser, // normalized to include id
|
||||||
referredBy: req.body.referred_by,
|
referredBy: req.body.referred_by,
|
||||||
|
|||||||
@ -261,6 +261,9 @@ const createDatabase = async () => {
|
|||||||
console.log('ℹ️ Phone columns already nullable or ALTER not required');
|
console.log('ℹ️ Phone columns already nullable or ALTER not required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ATU number for company profiles
|
||||||
|
await addColumnIfMissing(connection, 'company_profiles', 'atu_number', `VARCHAR(50) NULL AFTER registration_number`);
|
||||||
|
|
||||||
// 4. user_status table: Comprehensive tracking of user verification and completion steps
|
// 4. user_status table: Comprehensive tracking of user verification and completion steps
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
CREATE TABLE IF NOT EXISTS user_status (
|
CREATE TABLE IF NOT EXISTS user_status (
|
||||||
@ -1116,6 +1119,30 @@ const createDatabase = async () => {
|
|||||||
ADD CONSTRAINT \`fk_abon_purchaser_user\` FOREIGN KEY (\`purchaser_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE SET NULL ON UPDATE CASCADE`
|
ADD CONSTRAINT \`fk_abon_purchaser_user\` FOREIGN KEY (\`purchaser_user_id\`) REFERENCES \`users\`(\`id\`) ON DELETE SET NULL ON UPDATE CASCADE`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Contract fields
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'contract_number', `VARCHAR(50) NULL AFTER purchaser_user_id`);
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'contract_storage_key', `VARCHAR(255) NULL AFTER contract_number`);
|
||||||
|
|
||||||
|
// Additional shipping / contact fields
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'phone', `VARCHAR(50) NULL AFTER country`);
|
||||||
|
|
||||||
|
// Contract recipient
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'recipient_name', `VARCHAR(255) NULL AFTER phone`);
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'recipient_address', `TEXT NULL AFTER recipient_name`);
|
||||||
|
|
||||||
|
// Payment
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'payment_method', `VARCHAR(30) NULL AFTER frequency`);
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_by_email', `TINYINT(1) NOT NULL DEFAULT 0 AFTER payment_method`);
|
||||||
|
|
||||||
|
// Invoice address
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_same_as_shipping', `TINYINT(1) NOT NULL DEFAULT 1 AFTER invoice_by_email`);
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_full_name', `VARCHAR(200) NULL AFTER invoice_same_as_shipping`);
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_street', `VARCHAR(255) NULL AFTER invoice_full_name`);
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_postal_code', `VARCHAR(20) NULL AFTER invoice_street`);
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_city', `VARCHAR(100) NULL AFTER invoice_postal_code`);
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_phone', `VARCHAR(50) NULL AFTER invoice_city`);
|
||||||
|
await addColumnIfMissing(connection, 'coffee_abonements', 'invoice_email', `VARCHAR(255) NULL AFTER invoice_phone`);
|
||||||
|
|
||||||
// --- Coffee Abonement History ---
|
// --- Coffee Abonement History ---
|
||||||
await connection.query(`
|
await connection.query(`
|
||||||
CREATE TABLE IF NOT EXISTS coffee_abonement_history (
|
CREATE TABLE IF NOT EXISTS coffee_abonement_history (
|
||||||
|
|||||||
@ -26,6 +26,19 @@ class Abonemment {
|
|||||||
this.user_id = row.user_id ?? null; // NEW: map owner user_id
|
this.user_id = row.user_id ?? null; // NEW: map owner user_id
|
||||||
this.contract_number = row.contract_number ?? null;
|
this.contract_number = row.contract_number ?? null;
|
||||||
this.contract_storage_key = row.contract_storage_key ?? null;
|
this.contract_storage_key = row.contract_storage_key ?? null;
|
||||||
|
// Contact / invoice fields
|
||||||
|
this.phone = row.phone ?? null;
|
||||||
|
this.recipient_name = row.recipient_name ?? null;
|
||||||
|
this.recipient_address = row.recipient_address ?? null;
|
||||||
|
this.payment_method = row.payment_method ?? null;
|
||||||
|
this.invoice_by_email = !!row.invoice_by_email;
|
||||||
|
this.invoice_same_as_shipping = row.invoice_same_as_shipping !== undefined ? !!row.invoice_same_as_shipping : true;
|
||||||
|
this.invoice_full_name = row.invoice_full_name ?? null;
|
||||||
|
this.invoice_street = row.invoice_street ?? null;
|
||||||
|
this.invoice_postal_code = row.invoice_postal_code ?? null;
|
||||||
|
this.invoice_city = row.invoice_city ?? null;
|
||||||
|
this.invoice_phone = row.invoice_phone ?? null;
|
||||||
|
this.invoice_email = row.invoice_email ?? null;
|
||||||
this.created_at = row.created_at;
|
this.created_at = row.created_at;
|
||||||
this.updated_at = row.updated_at;
|
this.updated_at = row.updated_at;
|
||||||
}
|
}
|
||||||
|
|||||||
3
package-lock.json
generated
3
package-lock.json
generated
@ -2644,8 +2644,7 @@
|
|||||||
"version": "0.0.1581282",
|
"version": "0.0.1581282",
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
|
||||||
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
|
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/dfa": {
|
"node_modules/dfa": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|||||||
@ -74,6 +74,28 @@ class AbonemmentRepository {
|
|||||||
vals.push(snapshot.purchaser_user_id ?? null);
|
vals.push(snapshot.purchaser_user_id ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New contract / contact / invoice columns
|
||||||
|
const optionalCols = [
|
||||||
|
['phone', snapshot.phone || null],
|
||||||
|
['recipient_name', snapshot.recipient_name || null],
|
||||||
|
['recipient_address', snapshot.recipient_address || null],
|
||||||
|
['payment_method', snapshot.payment_method || null],
|
||||||
|
['invoice_by_email', snapshot.invoice_by_email ? 1 : 0],
|
||||||
|
['invoice_same_as_shipping', snapshot.invoice_same_as_shipping !== false ? 1 : 0],
|
||||||
|
['invoice_full_name', snapshot.invoice_full_name || null],
|
||||||
|
['invoice_street', snapshot.invoice_street || null],
|
||||||
|
['invoice_postal_code', snapshot.invoice_postal_code || null],
|
||||||
|
['invoice_city', snapshot.invoice_city || null],
|
||||||
|
['invoice_phone', snapshot.invoice_phone || null],
|
||||||
|
['invoice_email', snapshot.invoice_email || null],
|
||||||
|
];
|
||||||
|
for (const [col, val] of optionalCols) {
|
||||||
|
if (await this.hasColumn(col)) {
|
||||||
|
cols.push(col);
|
||||||
|
vals.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const placeholders = cols.map(() => '?').join(', ');
|
const placeholders = cols.map(() => '?').join(', ');
|
||||||
console.log('[CREATE ABONEMENT] Final columns:', cols);
|
console.log('[CREATE ABONEMENT] Final columns:', cols);
|
||||||
console.log('[CREATE ABONEMENT] Final values preview:', {
|
console.log('[CREATE ABONEMENT] Final values preview:', {
|
||||||
|
|||||||
@ -1,15 +1,48 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const puppeteer = require('puppeteer');
|
const puppeteer = require('puppeteer');
|
||||||
const { PutObjectCommand } = require('@aws-sdk/client-s3');
|
const { PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
const { s3: sharedExoscaleClient } = require('../../utils/exoscaleUploader');
|
const { s3: sharedExoscaleClient } = require('../../utils/exoscaleUploader');
|
||||||
const DocumentTemplateService = require('../template/DocumentTemplateService');
|
const DocumentTemplateService = require('../template/DocumentTemplateService');
|
||||||
const MailService = require('../email/MailService');
|
const MailService = require('../email/MailService');
|
||||||
|
const pool = require('../../database/database');
|
||||||
const { logger } = require('../../middleware/logger');
|
const { logger } = require('../../middleware/logger');
|
||||||
|
|
||||||
class AboContractService {
|
class AboContractService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.templatePath = path.join(__dirname, '..', '..', 'templates', 'abo', 'abo-contract-template.html');
|
this.templatePath = path.join(__dirname, '..', '..', 'templates', 'abo', 'abo-contract-template-new.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the latest active abo contract template from the contract management system (DB + S3).
|
||||||
|
* Falls back to the local file if no active template is found.
|
||||||
|
*/
|
||||||
|
async _loadTemplate(userType = 'both') {
|
||||||
|
try {
|
||||||
|
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract', 'abo');
|
||||||
|
if (latest?.storageKey) {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: process.env.EXOSCALE_BUCKET,
|
||||||
|
Key: latest.storageKey,
|
||||||
|
});
|
||||||
|
const fileObj = await sharedExoscaleClient.send(command);
|
||||||
|
if (fileObj.Body) {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of fileObj.Body) {
|
||||||
|
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
||||||
|
}
|
||||||
|
const html = Buffer.concat(chunks).toString('utf-8');
|
||||||
|
if (html.trim()) {
|
||||||
|
logger.info('AboContractService:template_loaded_from_s3', { id: latest.id, storageKey: latest.storageKey });
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('AboContractService:s3_template_load_failed', { message: e?.message });
|
||||||
|
}
|
||||||
|
// Fallback: local file
|
||||||
|
return fs.readFileSync(this.templatePath, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
_escapeHtml(value) {
|
_escapeHtml(value) {
|
||||||
@ -87,50 +120,148 @@ class AboContractService {
|
|||||||
abonement,
|
abonement,
|
||||||
actorUser,
|
actorUser,
|
||||||
contractNumber,
|
contractNumber,
|
||||||
|
signingCity,
|
||||||
signatureDataUrl,
|
signatureDataUrl,
|
||||||
|
isForSelf,
|
||||||
lang = 'en',
|
lang = 'en',
|
||||||
}) {
|
}) {
|
||||||
if (!abonement?.id) throw new Error('abonement is required');
|
if (!abonement?.id) throw new Error('abonement is required');
|
||||||
|
|
||||||
|
// Load template from contract management system (DB + S3), fallback to local file
|
||||||
|
const userType = await this._resolveUserType(actorUser);
|
||||||
let template;
|
let template;
|
||||||
try {
|
try {
|
||||||
template = fs.readFileSync(this.templatePath, 'utf8');
|
template = await this._loadTemplate(userType);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('AboContractService:template_missing', { templatePath: this.templatePath, message: e?.message });
|
logger.error('AboContractService:template_missing', { message: e?.message });
|
||||||
throw new Error('ABO contract template missing');
|
throw new Error('ABO contract template missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayContractNumber = String(contractNumber || '').trim() || `ABO-${abonement.id}`;
|
// --- Sequential contract number ---
|
||||||
|
let displayContractNumber = String(contractNumber || '').trim();
|
||||||
|
if (!displayContractNumber) {
|
||||||
|
displayContractNumber = await this._generateSequentialContractNumber();
|
||||||
|
}
|
||||||
const contractKeyPart = this._sanitizeKeyPart(displayContractNumber, `abo-${abonement.id}`);
|
const contractKeyPart = this._sanitizeKeyPart(displayContractNumber, `abo-${abonement.id}`);
|
||||||
|
|
||||||
let profitplanetSignature = '';
|
let profitplanetSignature = '';
|
||||||
try {
|
try {
|
||||||
const sig = await DocumentTemplateService.getProfitPlanetSignatureTag({ maxW: 300, maxH: 300 });
|
const sig = await DocumentTemplateService.getProfitPlanetSignatureTag({ maxW: 300, maxH: 300 });
|
||||||
profitplanetSignature = sig?.tag || '';
|
profitplanetSignature = sig?.tag || '';
|
||||||
|
logger.info('AboContractService:stamp_result', { reason: sig?.reason, hasTag: !!profitplanetSignature, tagLen: profitplanetSignature.length });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('AboContractService:getProfitPlanetSignatureTag_failed', { message: e?.message });
|
logger.warn('AboContractService:getProfitPlanetSignatureTag_failed', { message: e?.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Determine user type from actorUser or DB ---
|
||||||
|
// (userType already resolved above for template loading)
|
||||||
|
const isCompany = userType === 'company';
|
||||||
|
|
||||||
|
// --- Company data (FN / ATU) ---
|
||||||
|
let fnNumber = '';
|
||||||
|
let atuNumber = '';
|
||||||
|
if (isCompany && actorUser?.id) {
|
||||||
|
const companyData = await this._loadCompanyData(actorUser.id);
|
||||||
|
fnNumber = companyData.registrationNumber || '';
|
||||||
|
atuNumber = companyData.atuNumber || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Shipping & Invoice data ---
|
||||||
|
const fullName = `${abonement.first_name || ''} ${abonement.last_name || ''}`.trim();
|
||||||
|
const invoiceSame = abonement.invoice_same_as_shipping !== false;
|
||||||
|
|
||||||
|
// DEBUG: log abonement data to diagnose empty fields in PDF
|
||||||
|
logger.info('AboContractService:abonement_data', {
|
||||||
|
id: abonement.id,
|
||||||
|
first_name: abonement.first_name,
|
||||||
|
last_name: abonement.last_name,
|
||||||
|
email: abonement.email,
|
||||||
|
street: abonement.street,
|
||||||
|
postal_code: abonement.postal_code,
|
||||||
|
city: abonement.city,
|
||||||
|
phone: abonement.phone,
|
||||||
|
payment_method: abonement.payment_method,
|
||||||
|
invoice_by_email: abonement.invoice_by_email,
|
||||||
|
invoice_same_as_shipping: abonement.invoice_same_as_shipping,
|
||||||
|
recipient_name: abonement.recipient_name,
|
||||||
|
fullName,
|
||||||
|
invoiceSame,
|
||||||
|
});
|
||||||
|
|
||||||
const variables = {
|
const variables = {
|
||||||
|
// Meta
|
||||||
contractNumber: displayContractNumber,
|
contractNumber: displayContractNumber,
|
||||||
currentDate: this._formatDateTime(new Date()),
|
currentDate: this._formatDateTime(new Date()),
|
||||||
firstName: abonement.first_name || '',
|
|
||||||
lastName: abonement.last_name || '',
|
// Empfänger (An die) — auto-fill from shipping data if no explicit recipient set
|
||||||
email: abonement.email || '',
|
recipientName: abonement.recipient_name || fullName,
|
||||||
street: abonement.street || '',
|
recipientAddress: abonement.recipient_address || `${abonement.street || ''}, ${abonement.postal_code || ''} ${abonement.city || ''}`.trim(),
|
||||||
postalCode: abonement.postal_code || '',
|
|
||||||
city: abonement.city || '',
|
// Shipping
|
||||||
country: abonement.country || '',
|
shippingCustomerClass: isCompany ? '' : 'checked',
|
||||||
frequency: abonement.frequency || '',
|
shippingCompanyClass: isCompany ? 'checked' : '',
|
||||||
price: abonement.price != null ? String(abonement.price) : '',
|
shippingFullName: fullName,
|
||||||
currency: abonement.currency || 'EUR',
|
shippingStreet: abonement.street || '',
|
||||||
|
shippingPostalCode: abonement.postal_code || '',
|
||||||
|
shippingCity: abonement.city || '',
|
||||||
|
shippingPhone: abonement.phone || '',
|
||||||
|
shippingEmail: abonement.email || '',
|
||||||
|
|
||||||
|
// Invoice same as shipping
|
||||||
|
invoiceSameAsShippingMark: invoiceSame ? '✓' : '',
|
||||||
|
|
||||||
|
// Invoice address
|
||||||
|
invoiceCompanyClass: isCompany ? 'checked' : '',
|
||||||
|
invoiceCustomerClass: isCompany ? '' : 'checked',
|
||||||
|
invoiceFullName: invoiceSame ? fullName : (abonement.invoice_full_name || ''),
|
||||||
|
invoiceStreet: invoiceSame ? (abonement.street || '') : (abonement.invoice_street || ''),
|
||||||
|
invoicePostalCode: invoiceSame ? (abonement.postal_code || '') : (abonement.invoice_postal_code || ''),
|
||||||
|
invoiceCity: invoiceSame ? (abonement.city || '') : (abonement.invoice_city || ''),
|
||||||
|
invoicePhone: invoiceSame ? (abonement.phone || '') : (abonement.invoice_phone || ''),
|
||||||
|
invoiceEmail: invoiceSame ? (abonement.email || '') : (abonement.invoice_email || ''),
|
||||||
|
|
||||||
|
// Company numbers (FN / ATU) — only shown for company users
|
||||||
|
fnCheckedClass: isCompany && fnNumber ? 'checked' : '',
|
||||||
|
fnNumber,
|
||||||
|
atuCheckedClass: isCompany && atuNumber ? 'checked' : '',
|
||||||
|
atuNumber,
|
||||||
|
|
||||||
|
// Unternehmer / Konsument
|
||||||
|
entrepreneurClass: isCompany ? 'checked' : '',
|
||||||
|
consumerClass: isCompany ? '' : 'checked',
|
||||||
|
|
||||||
|
// Product selection
|
||||||
selectedProductsHtml: this._buildSelectedProductsHtml(abonement),
|
selectedProductsHtml: this._buildSelectedProductsHtml(abonement),
|
||||||
|
|
||||||
|
// Payment
|
||||||
|
paymentSepaClass: abonement.payment_method === 'sepa' ? 'checked' : '',
|
||||||
|
paymentCardClass: abonement.payment_method === 'card' ? 'checked' : '',
|
||||||
|
paymentSofortClass: abonement.payment_method === 'sofort' ? 'checked' : '',
|
||||||
|
invoiceByEmailClass: abonement.invoice_by_email ? 'checked' : '',
|
||||||
|
|
||||||
|
// Signatures
|
||||||
profitplanetSignature,
|
profitplanetSignature,
|
||||||
|
companyStampImage: profitplanetSignature,
|
||||||
signatureImage: this._buildSignatureImgHtml(signatureDataUrl),
|
signatureImage: this._buildSignatureImgHtml(signatureDataUrl),
|
||||||
|
signingCity: signingCity || '',
|
||||||
|
fullName,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// DEBUG: log template source and variable keys with values
|
||||||
|
logger.info('AboContractService:render_debug', {
|
||||||
|
templateSource: template ? (template.includes('{{shippingFullName}}') ? 'has_placeholders' : 'NO_placeholders') : 'empty',
|
||||||
|
templateLen: template?.length,
|
||||||
|
variableKeys: Object.keys(variables),
|
||||||
|
sampleValues: {
|
||||||
|
shippingFullName: variables.shippingFullName,
|
||||||
|
shippingStreet: variables.shippingStreet,
|
||||||
|
shippingEmail: variables.shippingEmail,
|
||||||
|
recipientName: variables.recipientName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const html = this._renderTemplate(template, variables, {
|
const html = this._renderTemplate(template, variables, {
|
||||||
rawKeys: new Set(['selectedProductsHtml', 'profitplanetSignature', 'signatureImage']),
|
rawKeys: new Set(['selectedProductsHtml', 'profitplanetSignature', 'signatureImage', 'companyStampImage']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const pdfBuffer = await this._renderPdfFromHtml(html);
|
const pdfBuffer = await this._renderPdfFromHtml(html);
|
||||||
@ -165,8 +296,7 @@ class AboContractService {
|
|||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|
||||||
await MailService.sendAboContractEmail({
|
const mailPayload = {
|
||||||
email: recipientEmail,
|
|
||||||
subject,
|
subject,
|
||||||
text,
|
text,
|
||||||
html: mailHtml,
|
html: mailHtml,
|
||||||
@ -175,13 +305,75 @@ class AboContractService {
|
|||||||
name: `${contractKeyPart}.pdf`,
|
name: `${contractKeyPart}.pdf`,
|
||||||
content: pdfBuffer.toString('base64'),
|
content: pdfBuffer.toString('base64'),
|
||||||
}],
|
}],
|
||||||
});
|
};
|
||||||
|
|
||||||
|
await MailService.sendAboContractEmail({ email: recipientEmail, ...mailPayload });
|
||||||
|
|
||||||
|
// When subscription is for someone else, also send the contract to the purchaser
|
||||||
|
const purchaserEmail = actorUser?.email;
|
||||||
|
const isForSomeoneElse = isForSelf === false;
|
||||||
|
if (isForSomeoneElse && purchaserEmail && purchaserEmail !== recipientEmail) {
|
||||||
|
try {
|
||||||
|
await MailService.sendAboContractEmail({ email: purchaserEmail, ...mailPayload });
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('AboContractService:purchaser_email_failed', { purchaserEmail, message: e?.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.warn('AboContractService:missing_recipient_email', { abonementId: abonement.id });
|
logger.warn('AboContractService:missing_recipient_email', { abonementId: abonement.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { storageKey: key, contractNumber: displayContractNumber };
|
return { storageKey: key, contractNumber: displayContractNumber };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate sequential contract number: ABO-YYYY-NNNNN
|
||||||
|
* Uses MAX(id) from coffee_abonements as a simple sequence.
|
||||||
|
*/
|
||||||
|
async _generateSequentialContractNumber() {
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT COALESCE(MAX(id), 0) + 1 AS next_seq FROM coffee_abonements`
|
||||||
|
);
|
||||||
|
const seq = rows[0]?.next_seq || 1;
|
||||||
|
return `ABO-${year}-${String(seq).padStart(5, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve user type from actorUser JWT payload or DB lookup.
|
||||||
|
*/
|
||||||
|
async _resolveUserType(actorUser) {
|
||||||
|
const fromToken = actorUser?.userType || actorUser?.user_type;
|
||||||
|
if (fromToken) return fromToken;
|
||||||
|
if (!actorUser?.id) return 'personal';
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT user_type FROM users WHERE id = ? LIMIT 1`,
|
||||||
|
[actorUser.id]
|
||||||
|
);
|
||||||
|
return rows[0]?.user_type || 'personal';
|
||||||
|
} catch {
|
||||||
|
return 'personal';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load company-specific data (FN number, ATU) for a company user.
|
||||||
|
*/
|
||||||
|
async _loadCompanyData(userId) {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT registration_number, atu_number FROM company_profiles WHERE user_id = ? LIMIT 1`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
registrationNumber: rows[0]?.registration_number || '',
|
||||||
|
atuNumber: rows[0]?.atu_number || '',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { registrationNumber: '', atuNumber: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = AboContractService;
|
module.exports = AboContractService;
|
||||||
|
|||||||
@ -49,7 +49,6 @@ class AbonemmentService {
|
|||||||
isForSelf,
|
isForSelf,
|
||||||
recipientName,
|
recipientName,
|
||||||
recipientEmail,
|
recipientEmail,
|
||||||
recipientNotes,
|
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email,
|
||||||
@ -58,8 +57,20 @@ class AbonemmentService {
|
|||||||
city,
|
city,
|
||||||
country,
|
country,
|
||||||
frequency,
|
frequency,
|
||||||
startDate,
|
phone,
|
||||||
|
recipientContractName,
|
||||||
|
recipientAddress,
|
||||||
|
paymentMethod,
|
||||||
|
invoiceByEmail,
|
||||||
|
invoiceSameAsShipping,
|
||||||
|
invoiceFullName,
|
||||||
|
invoiceStreet,
|
||||||
|
invoicePostalCode,
|
||||||
|
invoiceCity,
|
||||||
|
invoicePhone,
|
||||||
|
invoiceEmail,
|
||||||
contractNumber,
|
contractNumber,
|
||||||
|
signingCity,
|
||||||
signatureDataUrl,
|
signatureDataUrl,
|
||||||
actorUser,
|
actorUser,
|
||||||
referredBy, // NEW: referred_by field
|
referredBy, // NEW: referred_by field
|
||||||
@ -75,7 +86,6 @@ class AbonemmentService {
|
|||||||
city,
|
city,
|
||||||
country,
|
country,
|
||||||
frequency,
|
frequency,
|
||||||
startDate,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizedEmail = this.normalizeEmail(email);
|
const normalizedEmail = this.normalizeEmail(email);
|
||||||
@ -117,15 +127,14 @@ class AbonemmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const startDateObj = startDate ? new Date(startDate) : now;
|
const nextBilling = this.addInterval(now, billingInterval || 'month', intervalCount || 1);
|
||||||
const nextBilling = this.addInterval(startDateObj, billingInterval || 'month', intervalCount || 1);
|
|
||||||
|
|
||||||
const effectiveRecipientName = recipientName || `${firstName || ''} ${lastName || ''}`.trim() || null;
|
const effectiveRecipientName = recipientName || `${firstName || ''} ${lastName || ''}`.trim() || null;
|
||||||
const effectiveEmail = forSelf ? normalizedEmail : normalizedRecipientEmail;
|
const effectiveEmail = forSelf ? normalizedEmail : normalizedRecipientEmail;
|
||||||
|
|
||||||
const snapshot = {
|
const snapshot = {
|
||||||
status: 'active',
|
status: 'active',
|
||||||
started_at: startDateObj,
|
started_at: now,
|
||||||
next_billing_at: nextBilling,
|
next_billing_at: nextBilling,
|
||||||
billing_interval: billingInterval || 'month',
|
billing_interval: billingInterval || 'month',
|
||||||
interval_count: intervalCount || 1,
|
interval_count: intervalCount || 1,
|
||||||
@ -138,7 +147,6 @@ class AbonemmentService {
|
|||||||
total_packs: totalPacks,
|
total_packs: totalPacks,
|
||||||
is_for_self: forSelf,
|
is_for_self: forSelf,
|
||||||
recipient_name: forSelf ? null : effectiveRecipientName,
|
recipient_name: forSelf ? null : effectiveRecipientName,
|
||||||
recipient_notes: forSelf ? null : (recipientNotes || null)
|
|
||||||
},
|
},
|
||||||
pack_breakdown: breakdown,
|
pack_breakdown: breakdown,
|
||||||
first_name: forSelf ? firstName : (effectiveRecipientName || firstName),
|
first_name: forSelf ? firstName : (effectiveRecipientName || firstName),
|
||||||
@ -152,6 +160,19 @@ class AbonemmentService {
|
|||||||
referred_by: referredBy || (forSelf ? (actorUser?.id ?? null) : null),
|
referred_by: referredBy || (forSelf ? (actorUser?.id ?? null) : null),
|
||||||
user_id: forSelf ? (actorUser?.id ?? null) : null,
|
user_id: forSelf ? (actorUser?.id ?? null) : null,
|
||||||
purchaser_user_id: actorUser?.id ?? null, // NEW: also store purchaser
|
purchaser_user_id: actorUser?.id ?? null, // NEW: also store purchaser
|
||||||
|
// New fields
|
||||||
|
phone: phone || null,
|
||||||
|
recipient_name: recipientContractName || null,
|
||||||
|
recipient_address: recipientAddress || null,
|
||||||
|
payment_method: paymentMethod || null,
|
||||||
|
invoice_by_email: !!invoiceByEmail,
|
||||||
|
invoice_same_as_shipping: invoiceSameAsShipping !== false,
|
||||||
|
invoice_full_name: invoiceFullName || null,
|
||||||
|
invoice_street: invoiceStreet || null,
|
||||||
|
invoice_postal_code: invoicePostalCode || null,
|
||||||
|
invoice_city: invoiceCity || null,
|
||||||
|
invoice_phone: invoicePhone || null,
|
||||||
|
invoice_email: invoiceEmail || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[SUBSCRIBE ORDER] Snapshot user linking:', {
|
console.log('[SUBSCRIBE ORDER] Snapshot user linking:', {
|
||||||
@ -179,7 +200,9 @@ class AbonemmentService {
|
|||||||
abonement,
|
abonement,
|
||||||
actorUser,
|
actorUser,
|
||||||
contractNumber,
|
contractNumber,
|
||||||
|
signingCity,
|
||||||
signatureDataUrl,
|
signatureDataUrl,
|
||||||
|
isForSelf: forSelf,
|
||||||
lang,
|
lang,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -314,6 +337,7 @@ class AbonemmentService {
|
|||||||
recipientEmail,
|
recipientEmail,
|
||||||
recipientNotes,
|
recipientNotes,
|
||||||
contractNumber,
|
contractNumber,
|
||||||
|
signingCity,
|
||||||
signatureDataUrl,
|
signatureDataUrl,
|
||||||
actorUser,
|
actorUser,
|
||||||
referredBy, // NEW: referred_by field
|
referredBy, // NEW: referred_by field
|
||||||
@ -415,7 +439,9 @@ class AbonemmentService {
|
|||||||
abonement,
|
abonement,
|
||||||
actorUser,
|
actorUser,
|
||||||
contractNumber,
|
contractNumber,
|
||||||
|
signingCity,
|
||||||
signatureDataUrl,
|
signatureDataUrl,
|
||||||
|
isForSelf: isForMe,
|
||||||
lang,
|
lang,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user