269 lines
8.9 KiB
JavaScript
269 lines
8.9 KiB
JavaScript
const { UserTypes, Errors } = require('./constants'),
|
|
User = require('../model/user'),
|
|
{ AppError, AppAuthError } = require('./app_error');
|
|
|
|
/**
|
|
* Extract country name from a country field that may be a string code or populated Country object
|
|
* @param {Object|String} countryField - Country object or country code
|
|
* @return {String} The country name or code
|
|
*/
|
|
function getCountryName(countryField) {
|
|
if (!countryField) return '';
|
|
|
|
// If it's a populated Country object with a name property
|
|
if (typeof countryField === 'object' && countryField.name) {
|
|
return countryField.name;
|
|
}
|
|
|
|
// If it's a string (country code), return as is
|
|
return countryField;
|
|
}
|
|
|
|
/**
|
|
* Get the billing address from a customer object
|
|
* @param {Object} customer The customer object that may have billAddress or addresses array
|
|
* @returns {Object|null} The billing address or null if not found
|
|
*/
|
|
function getBillingAddressFromCustomer(customer) {
|
|
if (!customer) return null;
|
|
|
|
// If customer still has the old billAddress structure, use it
|
|
if (customer.billAddress) return { ...customer.billAddress };
|
|
|
|
// Otherwise, find the billing address in the addresses array
|
|
if (Array.isArray(customer.addresses) && customer.addresses.length > 0) {
|
|
// First try to find an address marked as billing
|
|
const billingAddress = customer.addresses.find(addr => addr.isBilling === true);
|
|
if (billingAddress) return { ...billingAddress };
|
|
|
|
// If no billing address is explicitly marked, use the first address
|
|
return { ...customer.addresses[0] };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Ensures only one billing address exists in an addresses array
|
|
* @param {Array} addresses - Array of addresses to process
|
|
* @returns {Array} Processed addresses with only one billing address
|
|
*/
|
|
function ensureSingleBillingAddress(addresses) {
|
|
if (!addresses || !Array.isArray(addresses)) return addresses;
|
|
|
|
let billingCount = 0;
|
|
let lastBillingIndex = -1;
|
|
|
|
// Count billing addresses and find the last one
|
|
addresses.forEach((address, index) => {
|
|
if (address.isBilling === true) {
|
|
billingCount++;
|
|
lastBillingIndex = index;
|
|
}
|
|
});
|
|
|
|
// If multiple billing addresses, keep only the last one as isBilling = true
|
|
if (billingCount > 1) {
|
|
return addresses.map((address, index) => {
|
|
if (address.isBilling === true && index !== lastBillingIndex) {
|
|
return { ...address, isBilling: false };
|
|
}
|
|
return address;
|
|
});
|
|
}
|
|
|
|
// If no billing address and only one address exists, make it billing
|
|
if (billingCount === 0 && addresses.length === 1) {
|
|
return addresses.map((address, index) =>
|
|
index === 0 ? { ...address, isBilling: true } : address
|
|
);
|
|
}
|
|
|
|
return addresses;
|
|
}
|
|
|
|
function getPuid(req, isObject = true, roles = []) {
|
|
let puid;
|
|
if (req.ut === UserTypes.APP) {
|
|
puid = req.uid;
|
|
} else if (roles.includes(req.ut)) {
|
|
if (!req.userInfo?.puid) {
|
|
AppError.throw(Errors.PARENT_NOT_EXIST);
|
|
}
|
|
puid = req.userInfo?.puid;
|
|
}
|
|
if (puid) return isObject ? { byPuid: puid } : puid;
|
|
AppError.throw(Errors.NO_ACCESS);
|
|
}
|
|
|
|
async function checkUserClient(userId, req) {
|
|
const user = await User.findOne({
|
|
_id: userId,
|
|
kind: UserTypes.CLIENT,
|
|
parent: getPuid(req, false),
|
|
});
|
|
if (!user) {
|
|
AppAuthError.throw(Errors.USER_NOT_FOUND);
|
|
}
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Get a formatted address string from the user's address field or optionally from addresses array
|
|
* @param {Object} user - User object with addresses array or address string
|
|
* @param {Boolean} useBillingAddress - If true, prioritize billing address from addresses array
|
|
* @return {String} Formatted address string
|
|
*/
|
|
function getFormattedAddress(user, useBillingAddress = false) {
|
|
if (!user) return '';
|
|
|
|
// If useBillingAddress is true, try addresses array first
|
|
if (useBillingAddress && user.addresses && Array.isArray(user.addresses) && user.addresses.length > 0) {
|
|
// Try to find billing address first
|
|
const billingAddress = user.addresses.find(addr => addr.isBilling === true);
|
|
// Use billing address if found, otherwise use the first address as primary
|
|
const primaryAddress = billingAddress || user.addresses[0];
|
|
|
|
// Skip if the address has no meaningful data
|
|
if (!primaryAddress || Object.keys(primaryAddress).filter(k => k !== 'isBilling' && primaryAddress[k]).length === 0) {
|
|
// Fall through to next methods if address is empty
|
|
} else {
|
|
// Get country name - check if country is a populated object or just a code
|
|
let country = '';
|
|
if (primaryAddress.country) {
|
|
if (typeof primaryAddress.country === 'object' && primaryAddress.country.name) {
|
|
// It's a populated country object
|
|
country = primaryAddress.country.name;
|
|
} else if (typeof primaryAddress.Country === 'object' && primaryAddress.Country.name) {
|
|
// It's a populated Country object
|
|
country = primaryAddress.Country.name;
|
|
} else {
|
|
// It's a country code string
|
|
country = primaryAddress.country;
|
|
}
|
|
}
|
|
|
|
const addressParts = [
|
|
primaryAddress.line1,
|
|
primaryAddress.line2,
|
|
primaryAddress.city,
|
|
primaryAddress.state,
|
|
primaryAddress.postalCode || primaryAddress.postal_code,
|
|
country
|
|
].filter(Boolean); // Remove empty parts
|
|
|
|
if (addressParts.length > 0) {
|
|
return addressParts.join(', ');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default precedence: legacy address field (highest precedence when useBillingAddress is false)
|
|
if (user.address) {
|
|
return user.address;
|
|
}
|
|
|
|
// Check for populated top-level Country object (typically from mongoose population)
|
|
if (user.Country && typeof user.Country === 'object' && user.Country.name) {
|
|
return user.Country.name;
|
|
}
|
|
|
|
// Last resort: just return the country code if available
|
|
return user.country || '';
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate country object or value from a user/customer document
|
|
* @param {Object} doc - Document that may have Country and/or addresses.country
|
|
* @param {Boolean} nameOnly - If true, returns just the country name string
|
|
* @returns {Object|String} The country object or name
|
|
*/
|
|
function getDocumentCountry(doc, nameOnly = false) {
|
|
if (!doc) return nameOnly ? '' : null;
|
|
|
|
// Check for populated top-level Country reference first
|
|
if (doc.Country && typeof doc.Country === 'object' && doc.Country.name) {
|
|
return nameOnly ? doc.Country.name : doc.Country;
|
|
}
|
|
|
|
// Check for country inside billing address
|
|
if (Array.isArray(doc.addresses) && doc.addresses.length > 0) {
|
|
const billingAddress = doc.addresses.find(addr => addr.isBilling === true) || doc.addresses[0];
|
|
if (billingAddress && billingAddress.country) {
|
|
if (typeof billingAddress.country === 'object' && billingAddress.country.name) {
|
|
return nameOnly ? billingAddress.country.name : billingAddress.country;
|
|
} else if (typeof billingAddress.country === 'string') {
|
|
return nameOnly ? billingAddress.country : { code: billingAddress.country };
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to string country code
|
|
if (doc.country) {
|
|
return nameOnly ? doc.country : { code: doc.country };
|
|
}
|
|
|
|
return nameOnly ? '' : null;
|
|
}
|
|
|
|
/**
|
|
* Update addresses array ensuring only one billing address exists
|
|
* @param {Array} addresses - Current addresses array
|
|
* @param {Object} newAddress - New address to set as billing
|
|
* @param {string} existingId - ID of existing address to update (optional)
|
|
* @returns {Array} Updated addresses array with single billing address
|
|
*/
|
|
function updateBillingAddress(addresses, newAddress, existingId = null) {
|
|
// Handle case where addresses might be null/undefined
|
|
const updatedAddresses = [...(addresses || [])];
|
|
|
|
// Mark all addresses as non-billing first
|
|
updatedAddresses.forEach((addr, index) => {
|
|
if (addr.isBilling === true) {
|
|
updatedAddresses[index] = { ...addr, isBilling: false };
|
|
}
|
|
});
|
|
|
|
if (existingId) {
|
|
// Update existing address by ID
|
|
const existingIndex = updatedAddresses.findIndex(addr =>
|
|
addr._id && addr._id.toString() === existingId.toString()
|
|
);
|
|
if (existingIndex >= 0) {
|
|
updatedAddresses[existingIndex] = { ...newAddress, _id: existingId, isBilling: true };
|
|
return updatedAddresses;
|
|
}
|
|
}
|
|
|
|
// Check for content match
|
|
const matchIndex = updatedAddresses.findIndex(addr =>
|
|
addr.line1 === newAddress.line1 &&
|
|
addr.line2 === newAddress.line2 &&
|
|
addr.city === newAddress.city &&
|
|
addr.state === newAddress.state &&
|
|
addr.postalCode === newAddress.postalCode &&
|
|
addr.country === newAddress.country
|
|
);
|
|
|
|
if (matchIndex >= 0) {
|
|
// Update existing address with matching content
|
|
updatedAddresses[matchIndex] = { ...updatedAddresses[matchIndex], isBilling: true };
|
|
} else {
|
|
// Add new billing address
|
|
updatedAddresses.push({ ...newAddress, isBilling: true });
|
|
}
|
|
|
|
return updatedAddresses;
|
|
}
|
|
|
|
module.exports = {
|
|
getPuid,
|
|
checkUserClient,
|
|
getFormattedAddress,
|
|
getCountryName,
|
|
getBillingAddressFromCustomer,
|
|
ensureSingleBillingAddress,
|
|
updateBillingAddress,
|
|
getDocumentCountry
|
|
};
|