pini-print-bot / engine /customerManager.js
dotandru's picture
20
f860279
Raw
History Blame Contribute Delete
12.5 kB
/**
* Customer Manager - Pini Print Bot
* ==================================
* ื ื™ื”ื•ืœ ืœืงื•ื—ื•ืช ืขื ื–ื™ื”ื•ื™ ืœืคื™ ื˜ืœืคื•ืŸ
* ื”ื™ืกื˜ื•ืจื™ื™ืช ื”ื–ืžื ื•ืช, ื”ืขื“ืคื•ืช, ื•-CRM ื‘ืกื™ืกื™
*/
// === ืžืื’ืจ ืœืงื•ื—ื•ืช (ื‘ื–ื™ื›ืจื•ืŸ - ื‘ืคืจื•ื“ืงืฉืŸ: MongoDB/PostgreSQL) ===
const customers = new Map();
// === ืžื‘ื ื” ืœืงื•ื— ===
function createCustomer(phone, name = null) {
return {
id: `cust_${Date.now()}`,
phone: normalizePhone(phone),
name: name,
email: null,
// ืกื˜ื˜ื™ืกื˜ื™ืงื•ืช
stats: {
totalOrders: 0,
totalSpent: 0,
firstOrder: null,
lastOrder: null,
averageOrder: 0
},
// ื”ืขื“ืคื•ืช ืฉื ืœืžื“ื•
preferences: {
preferredProducts: [], // ืžื•ืฆืจื™ื ืฉื”ื–ืžื™ืŸ ื”ื›ื™ ื”ืจื‘ื”
preferredMaterials: [], // ื—ื•ืžืจื™ื ืžื•ืขื“ืคื™ื
usualQuantities: {}, // ื›ืžื•ื™ื•ืช ืจื’ื™ืœื•ืช ืœืคื™ ืžื•ืฆืจ
priceRange: null, // ื˜ื•ื•ื— ืžื—ื™ืจื™ื ืจื’ื™ืœ
designStatus: 'unknown' // ื”ืื ื‘ื“"ื› ืžื’ื™ืข ืขื ืขื™ืฆื•ื‘
},
// ื”ื™ืกื˜ื•ืจื™ื”
orderHistory: [], // ื”ื–ืžื ื•ืช ืงื•ื“ืžื•ืช
quoteHistory: [], // ื”ืฆืขื•ืช ืžื—ื™ืจ (ื’ื ืืœื” ืฉืœื ื”ืคื›ื• ืœื”ื–ืžื ื”)
// ื”ืขืจื•ืช
notes: [], // ื”ืขืจื•ืช ืฉืœ ื‘ื™ืช ื”ื“ืคื•ืก
tags: [], // ืชื’ื™ื•ืช: 'VIP', 'ืขืกืงื™', 'ืคืจื˜ื™', 'ื‘ืขื™ื™ืชื™'
// ืžื˜ื
createdAt: new Date(),
updatedAt: new Date()
};
}
// === ื ืจืžื•ืœ ื˜ืœืคื•ืŸ ===
function normalizePhone(phone) {
if (!phone) return null;
// ื”ืกืจ ื›ืœ ืžื” ืฉืœื ืกืคืจื”
let normalized = phone.replace(/\D/g, '');
// ื”ืžืจ 972 ืœ-0
if (normalized.startsWith('972')) {
normalized = '0' + normalized.slice(3);
}
// ื•ื•ื“ื ืฉืžืชื—ื™ืœ ื‘-0
if (!normalized.startsWith('0') && normalized.length === 9) {
normalized = '0' + normalized;
}
return normalized;
}
// === ื—ื™ืคื•ืฉ/ื™ืฆื™ืจืช ืœืงื•ื— ===
function findOrCreateCustomer(phone, name = null) {
const normalizedPhone = normalizePhone(phone);
if (!normalizedPhone) {
return null;
}
// ื—ืคืฉ ืœืงื•ื— ืงื™ื™ื
let customer = customers.get(normalizedPhone);
if (customer) {
// ืขื“ื›ืŸ ืฉื ืื ื ื™ืชืŸ ื—ื“ืฉ
if (name && !customer.name) {
customer.name = name;
customer.updatedAt = new Date();
}
console.log(` ๐Ÿ‘ค Found existing customer: ${customer.name || normalizedPhone}`);
return customer;
}
// ืฆื•ืจ ืœืงื•ื— ื—ื“ืฉ
customer = createCustomer(normalizedPhone, name);
customers.set(normalizedPhone, customer);
console.log(` ๐Ÿ‘ค Created new customer: ${name || normalizedPhone}`);
return customer;
}
// === ื—ื™ืคื•ืฉ ืœืงื•ื— ืœืคื™ ื˜ืœืคื•ืŸ ===
function getCustomerByPhone(phone) {
const normalizedPhone = normalizePhone(phone);
return customers.get(normalizedPhone) || null;
}
// === ืขื“ื›ื•ืŸ ืœืงื•ื— ืื—ืจื™ ื”ื–ืžื ื” ===
function updateCustomerAfterOrder(phone, order) {
const customer = getCustomerByPhone(phone);
if (!customer) return null;
// ืขื“ื›ืŸ ืกื˜ื˜ื™ืกื˜ื™ืงื•ืช
customer.stats.totalOrders++;
customer.stats.totalSpent += order.total;
customer.stats.lastOrder = new Date();
if (!customer.stats.firstOrder) {
customer.stats.firstOrder = new Date();
}
customer.stats.averageOrder = Math.round(customer.stats.totalSpent / customer.stats.totalOrders);
// ืขื“ื›ืŸ ื”ืขื“ืคื•ืช
order.items.forEach(item => {
// ืžื•ืฆืจื™ื ืžื•ืขื“ืคื™ื
const prodIndex = customer.preferences.preferredProducts.findIndex(p => p.name === item.product_name);
if (prodIndex >= 0) {
customer.preferences.preferredProducts[prodIndex].count++;
} else {
customer.preferences.preferredProducts.push({ name: item.product_name, count: 1 });
}
// ื›ืžื•ื™ื•ืช ืจื’ื™ืœื•ืช
if (!customer.preferences.usualQuantities[item.product_name]) {
customer.preferences.usualQuantities[item.product_name] = [];
}
customer.preferences.usualQuantities[item.product_name].push(item.qty);
});
// ืžื™ื™ืŸ ืžื•ืขื“ืคื™ื
customer.preferences.preferredProducts.sort((a, b) => b.count - a.count);
// ืฉืžื•ืจ ื‘ื”ื™ืกื˜ื•ืจื™ื”
customer.orderHistory.push({
id: `ord_${Date.now()}`,
date: new Date(),
items: order.items,
total: order.total,
status: 'confirmed'
});
customer.updatedAt = new Date();
return customer;
}
// === ืฉืžื™ืจืช ื”ืฆืขืช ืžื—ื™ืจ (ื’ื ืื ืœื ื”ืคื›ื” ืœื”ื–ืžื ื”) ===
function saveQuoteToCustomer(phone, cart, total) {
const customer = getCustomerByPhone(phone);
if (!customer) return null;
customer.quoteHistory.push({
id: `quote_${Date.now()}`,
date: new Date(),
items: cart.map(i => ({
product_name: i.product_name,
qty: i.qty,
price: i.client_price
})),
total,
convertedToOrder: false
});
customer.updatedAt = new Date();
return customer;
}
// === ื”ื•ืกืคืช ื”ืขืจื” ืœืœืงื•ื— ===
function addNoteToCustomer(phone, note, author = 'system') {
const customer = getCustomerByPhone(phone);
if (!customer) return null;
customer.notes.push({
text: note,
author,
date: new Date()
});
customer.updatedAt = new Date();
return customer;
}
// === ื”ื•ืกืคืช ืชื’ื™ืช ===
function addTagToCustomer(phone, tag) {
const customer = getCustomerByPhone(phone);
if (!customer) return null;
if (!customer.tags.includes(tag)) {
customer.tags.push(tag);
customer.updatedAt = new Date();
}
return customer;
}
// === ืงื‘ืœืช ืกื™ื›ื•ื ืœืงื•ื— ืœื“ืฉื‘ื•ืจื“ ===
function getCustomerSummary(phone) {
const customer = getCustomerByPhone(phone);
if (!customer) return null;
// ื—ืฉื‘ ื›ืžื•ืช ืจื’ื™ืœื” ืžืžื•ืฆืขืช ืœื›ืœ ืžื•ืฆืจ
const usualQuantities = {};
for (const [product, quantities] of Object.entries(customer.preferences.usualQuantities)) {
const avg = Math.round(quantities.reduce((a, b) => a + b, 0) / quantities.length);
usualQuantities[product] = avg;
}
return {
id: customer.id,
name: customer.name,
phone: customer.phone,
// ืชืžืฆื™ืช ืœื“ืฉื‘ื•ืจื“
isNew: customer.stats.totalOrders === 0,
isVIP: customer.tags.includes('VIP'),
isReturning: customer.stats.totalOrders > 0,
// ืกื˜ื˜ื™ืกื˜ื™ืงื•ืช
totalOrders: customer.stats.totalOrders,
totalSpent: customer.stats.totalSpent,
averageOrder: customer.stats.averageOrder,
daysSinceLastOrder: customer.stats.lastOrder
? Math.floor((Date.now() - new Date(customer.stats.lastOrder)) / (1000 * 60 * 60 * 24))
: null,
// ื”ืขื“ืคื•ืช
topProducts: customer.preferences.preferredProducts.slice(0, 3),
usualQuantities,
// ืชื’ื™ื•ืช ื•ื”ืขืจื•ืช
tags: customer.tags,
lastNote: customer.notes.length > 0
? customer.notes[customer.notes.length - 1]
: null,
// ื”ื™ืกื˜ื•ืจื™ื” ืื—ืจื•ื ื”
lastOrders: customer.orderHistory.slice(-3).reverse()
};
}
// === ื—ื™ืคื•ืฉ ืœืงื•ื—ื•ืช ===
function searchCustomers(query) {
const results = [];
const searchLower = query.toLowerCase();
for (const customer of customers.values()) {
if (
customer.phone.includes(query) ||
(customer.name && customer.name.toLowerCase().includes(searchLower)) ||
(customer.email && customer.email.toLowerCase().includes(searchLower))
) {
results.push(getCustomerSummary(customer.phone));
}
}
return results;
}
// === ืงื‘ืœืช ื›ืœ ื”ืœืงื•ื—ื•ืช (ืœื“ืฉื‘ื•ืจื“ ื ื™ื”ื•ืœ) ===
function getAllCustomers(options = {}) {
const {
sortBy = 'lastOrder',
limit = 50,
tags = null,
minSpent = null
} = options;
let results = Array.from(customers.values());
// ืกื™ื ื•ืŸ ืœืคื™ ืชื’ื™ื•ืช
if (tags && tags.length > 0) {
results = results.filter(c => tags.some(t => c.tags.includes(t)));
}
// ืกื™ื ื•ืŸ ืœืคื™ ืกื›ื•ื ืžื™ื ื™ืžืœื™
if (minSpent) {
results = results.filter(c => c.stats.totalSpent >= minSpent);
}
// ืžื™ื•ืŸ
switch (sortBy) {
case 'lastOrder':
results.sort((a, b) => (b.stats.lastOrder || 0) - (a.stats.lastOrder || 0));
break;
case 'totalSpent':
results.sort((a, b) => b.stats.totalSpent - a.stats.totalSpent);
break;
case 'totalOrders':
results.sort((a, b) => b.stats.totalOrders - a.stats.totalOrders);
break;
case 'name':
results.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
break;
}
// ื”ื’ื‘ืœื”
results = results.slice(0, limit);
return results.map(c => getCustomerSummary(c.phone));
}
// === ืกื˜ื˜ื™ืกื˜ื™ืงื•ืช ื›ืœืœื™ื•ืช ===
function getCustomerStats() {
const allCustomers = Array.from(customers.values());
const totalCustomers = allCustomers.length;
const totalRevenue = allCustomers.reduce((sum, c) => sum + c.stats.totalSpent, 0);
const totalOrders = allCustomers.reduce((sum, c) => sum + c.stats.totalOrders, 0);
const vipCustomers = allCustomers.filter(c => c.tags.includes('VIP')).length;
const newCustomers = allCustomers.filter(c => c.stats.totalOrders === 0).length;
const returningCustomers = allCustomers.filter(c => c.stats.totalOrders > 1).length;
// ืœืงื•ื—ื•ืช ืคืขื™ืœื™ื (ื”ื–ืžื™ื ื• ื‘-30 ื™ื•ื ื”ืื—ืจื•ื ื™ื)
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
const activeCustomers = allCustomers.filter(c =>
c.stats.lastOrder && new Date(c.stats.lastOrder) > thirtyDaysAgo
).length;
return {
totalCustomers,
totalRevenue,
totalOrders,
averageOrderValue: totalOrders > 0 ? Math.round(totalRevenue / totalOrders) : 0,
vipCustomers,
newCustomers,
returningCustomers,
activeCustomers,
conversionRate: totalCustomers > 0
? Math.round((returningCustomers / totalCustomers) * 100)
: 0
};
}
// === ื–ื™ื”ื•ื™ ื˜ืœืคื•ืŸ ืžื˜ืงืกื˜ ===
function extractPhoneFromText(text) {
// ื—ืคืฉ ืžืกืคืจ ื˜ืœืคื•ืŸ ื™ืฉืจืืœื™
const patterns = [
/0[5-9]\d[- ]?\d{3}[- ]?\d{4}/, // 050-123-4567 or 0501234567
/\+972[- ]?5\d[- ]?\d{3}[- ]?\d{4}/, // +972-50-123-4567
/972[- ]?5\d[- ]?\d{3}[- ]?\d{4}/ // 972-50-123-4567
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
return normalizePhone(match[0]);
}
}
return null;
}
// === ื–ื™ื”ื•ื™ ืฉื ืžื˜ืงืกื˜ ===
function extractNameFromText(text) {
// ื—ืคืฉ "ืื ื™ X" ืื• "ืงื•ืจืื™ื ืœื™ X" ืื• "ืฉืžื™ X"
const patterns = [
/(?:ืื ื™|ืฉืžื™|ืงื•ืจืื™ื ืœื™)\s+([ื-ืช]+(?:\s+[ื-ืช]+)?)/,
/(?:I'm|I am|my name is)\s+(\w+(?:\s+\w+)?)/i
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
return match[1].trim();
}
}
return null;
}
module.exports = {
// ื ื™ื”ื•ืœ ืœืงื•ื—ื•ืช
findOrCreateCustomer,
getCustomerByPhone,
updateCustomerAfterOrder,
saveQuoteToCustomer,
addNoteToCustomer,
addTagToCustomer,
// ืฉืœื™ืคืช ืžื™ื“ืข
getCustomerSummary,
searchCustomers,
getAllCustomers,
getCustomerStats,
// ืขื–ืจ
normalizePhone,
extractPhoneFromText,
extractNameFromText,
// ื’ื™ืฉื” ื™ืฉื™ืจื” (ืœื‘ื“ื™ืงื•ืช)
_customers: customers
};