diff --git a/server/src/config/museumMapping.ts b/server/src/config/museumMapping.ts index e5e4c57..ca0970a 100644 --- a/server/src/config/museumMapping.ts +++ b/server/src/config/museumMapping.ts @@ -1,5 +1,6 @@ // Definitive mapping of ERP product descriptions to museum names. // Priority order matters — first match wins (handles combo tickets). +// Combo tickets matching multiple museums split revenue/visits 50/50. const MUSEUM_KEYWORDS: [string, string[]][] = [ ['Revelation Exhibition', ['Revelation', 'الوحي']], @@ -7,19 +8,35 @@ const MUSEUM_KEYWORDS: [string, string[]][] = [ ['Holy Quraan Museum', ['Holy Quraan', 'القرآن الكريم']], ['Trail To Hira Cave', ['Trail To Hira', 'غار حراء']], ['Makkah Greets Us', ['Makkah Greets']], + ['Best of Creation', ['Best of Creation', 'خير الخلق']], ['VIP Experience', ['VIP Experience']], ]; export const MUSEUM_NAMES = MUSEUM_KEYWORDS.map(([name]) => name); -export function getMuseumFromProduct(productDescription: string): string { +export interface MuseumMatch { + museums: string[]; + split: number; // 1 = full, 0.5 = split between 2 museums +} + +export function getMuseumsFromProduct(productDescription: string): MuseumMatch { const desc = productDescription.trim(); + const matched: string[] = []; + for (const [museum, keywords] of MUSEUM_KEYWORDS) { for (const kw of keywords) { - if (desc.includes(kw)) return museum; + if (desc.includes(kw)) { + matched.push(museum); + break; + } } } - return 'Other'; + + if (matched.length === 0) return { museums: ['Other'], split: 1 }; + if (matched.length === 1) return { museums: matched, split: 1 }; + + // Multiple museums matched — split evenly + return { museums: matched, split: 1 / matched.length }; } export const CHANNEL_LABELS: Record = { diff --git a/server/src/services/etlSync.ts b/server/src/services/etlSync.ts index 505d18d..e777492 100644 --- a/server/src/services/etlSync.ts +++ b/server/src/services/etlSync.ts @@ -1,6 +1,6 @@ import { fetchSales } from './erpClient'; import { discoverTableIds, deleteRowsByMonth, deleteAllRows, insertRecords } from './nocodbClient'; -import { getMuseumFromProduct, getChannelLabel } from '../config/museumMapping'; +import { getMuseumsFromProduct, getChannelLabel } from '../config/museumMapping'; import type { ERPSaleRecord, AggregatedRecord } from '../types'; function generateMonthBoundaries(startYear: number, startMonth: number): Array<[string, string]> { @@ -43,19 +43,38 @@ export function aggregateTransactions(sales: ERPSaleRecord[]): AggregatedRecord[ const channel = getChannelLabel(sale.OperatingAreaName); for (const product of sale.Products) { - const museum = getMuseumFromProduct(product.ProductDescription); - const key = `${date}|${museum}|${channel}`; + const { museums, split } = getMuseumsFromProduct(product.ProductDescription); + const isCombo = museums.length > 1; - let entry = map.get(key); - if (!entry) { - entry = { Date: date, MuseumName: museum, Channel: channel, Visits: 0, Tickets: 0, GrossRevenue: 0, NetRevenue: 0 }; - map.set(key, entry); + for (const museum of museums) { + const comboWith = isCombo + ? museums.filter(m => m !== museum).join(', ') + : ''; + const ticketType = isCombo ? 'combo' : 'single'; + const key = `${date}|${museum}|${channel}|${ticketType}`; + + let entry = map.get(key); + if (!entry) { + entry = { + Date: date, + MuseumName: museum, + Channel: channel, + TicketType: ticketType, + ComboMuseums: museums.length, + ComboWith: comboWith, + Visits: 0, + Tickets: 0, + GrossRevenue: 0, + NetRevenue: 0, + }; + map.set(key, entry); + } + + entry.Visits += product.PeopleCount * split; + entry.Tickets += product.UnitQuantity * split; + entry.GrossRevenue += product.TotalPrice * split; + entry.NetRevenue += (product.TotalPrice - product.TaxAmount) * split; } - - entry.Visits += product.PeopleCount; - entry.Tickets += product.UnitQuantity; - entry.GrossRevenue += product.TotalPrice; - entry.NetRevenue += product.TotalPrice - product.TaxAmount; } } diff --git a/server/src/types.ts b/server/src/types.ts index bf1d9d9..befb231 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -20,6 +20,9 @@ export interface AggregatedRecord { Date: string; MuseumName: string; Channel: string; + TicketType: 'single' | 'combo'; + ComboMuseums: number; + ComboWith: string; Visits: number; Tickets: number; GrossRevenue: number; diff --git a/src/types/index.ts b/src/types/index.ts index d1308e5..ff4839f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -79,6 +79,9 @@ export interface NocoDBDailySale { Date: string; MuseumName: string; Channel: string; + TicketType: string; + ComboMuseums: number; + ComboWith: string; Visits: number; Tickets: number; GrossRevenue: number;