feat: enhance subscription model with additional contact and invoice fields

hawi i cant
This commit is contained in:
seaznCode 2026-03-16 20:35:10 +01:00
parent 46a081ae8f
commit 19847eefb4
7 changed files with 320 additions and 30 deletions

View File

@ -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,

View File

@ -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 (

View File

@ -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
View File

@ -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",

View File

@ -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:', {

View File

@ -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;

View File

@ -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,
}); });