UNPKG

@accounter/server

Version:
636 lines (611 loc) • 18.7 kB
import { Injectable, Scope } from 'graphql-modules'; import { sql } from '@pgtyped/runtime'; import type { AmexTransactionInput, ChangedField, ChangedTransaction, InsertedTransactionSummary, IsracardTransactionInput, ScraperUploadResult, } from '../../../__generated__/types.js'; import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js'; import { formatValue } from '../helpers/utils.helper.js'; import { validateIsracardAmexTransactions } from '../helpers/validators.helper.js'; import type { IFetchAmexByCardsQuery, IFetchAmexByCardsResult, IFetchIsracardByCardsQuery, IFetchIsracardByCardsResult, IUploadAmexTransactionsParams, IUploadAmexTransactionsQuery, IUploadAmexTransactionsResult, IUploadIsracardTransactionsParams, IUploadIsracardTransactionsQuery, IUploadIsracardTransactionsResult, } from '../types.js'; const fetchIsracardByCards = sql<IFetchIsracardByCardsQuery>` SELECT id, card, specific_date, card_index, deals_inbound, supplier_id, supplier_name, deal_sum_type, payment_sum_sign, purchase_date, full_purchase_date, more_info, horaat_keva, voucher_number, voucher_number_ratz, solek, purchase_date_outbound, full_purchase_date_outbound, currency_id, current_payment_currency, city, supplier_name_outbound, full_supplier_name_outbound, payment_date, full_payment_date, is_show_deals_outbound, adendum, voucher_number_ratz_outbound, is_show_link_for_supplier_details, deal_sum, payment_sum, full_supplier_name_heb, deal_sum_outbound, payment_sum_outbound, is_horaat_keva, stage, return_code, message, return_message, display_properties, table_page_num, is_error, is_captcha, is_button, site_name, kod_matbea_mekori, esb_services_call FROM accounter_schema.isracard_creditcard_transactions WHERE card = ANY($cards!) `; const uploadIsracardTransactions = sql<IUploadIsracardTransactionsQuery>` INSERT INTO accounter_schema.isracard_creditcard_transactions ( specific_date, card_index, deals_inbound, supplier_id, supplier_name, deal_sum_type, payment_sum_sign, purchase_date, full_purchase_date, more_info, horaat_keva, voucher_number, voucher_number_ratz, solek, purchase_date_outbound, full_purchase_date_outbound, currency_id, current_payment_currency, city, supplier_name_outbound, full_supplier_name_outbound, payment_date, full_payment_date, is_show_deals_outbound, adendum, voucher_number_ratz_outbound, is_show_link_for_supplier_details, deal_sum, payment_sum, full_supplier_name_heb, deal_sum_outbound, payment_sum_outbound, is_horaat_keva, stage, return_code, message, return_message, display_properties, table_page_num, is_error, is_captcha, is_button, site_name, client_ip_address, card, charging_date, kod_matbea_mekori, esb_services_call ) VALUES $$transactions( specificDate, cardIndex, dealsInbound, supplierId, supplierName, dealSumType, paymentSumSign, purchaseDate, fullPurchaseDate, moreInfo, horaatKeva, voucherNumber, voucherNumberRatz, solek, purchaseDateOutbound, fullPurchaseDateOutbound, currencyId, currentPaymentCurrency, city, supplierNameOutbound, fullSupplierNameOutbound, paymentDate, fullPaymentDate, isShowDealsOutbound, adendum, voucherNumberRatzOutbound, isShowLinkForSupplierDetails, dealSum, paymentSum, fullSupplierNameHeb, dealSumOutbound, paymentSumOutbound, isHoraatKeva, stage, returnCode, message, returnMessage, displayProperties, tablePageNum, isError, isCaptcha, isButton, siteName, clientIpAddress, card, chargingDate, kodMatbeaMekori, esbServicesCall ) ON CONFLICT ( card, COALESCE(full_purchase_date, full_purchase_date_outbound), full_payment_date, COALESCE(payment_sum, payment_sum_outbound), voucher_number, COALESCE(voucher_number_ratz, voucher_number_ratz_outbound), supplier_id, current_payment_currency, COALESCE(full_supplier_name_heb, full_supplier_name_outbound), more_info, deal_sum ) DO NOTHING RETURNING id, full_purchase_date, supplier_name, payment_sum, card; `; const fetchAmexByCards = sql<IFetchAmexByCardsQuery>` SELECT id, card, specific_date, card_index, deals_inbound, supplier_id, supplier_name, deal_sum_type, payment_sum_sign, purchase_date, full_purchase_date, more_info, horaat_keva, voucher_number, voucher_number_ratz, solek, purchase_date_outbound, full_purchase_date_outbound, currency_id, current_payment_currency, city, supplier_name_outbound, full_supplier_name_outbound, payment_date, full_payment_date, is_show_deals_outbound, adendum, voucher_number_ratz_outbound, is_show_link_for_supplier_details, deal_sum, payment_sum, full_supplier_name_heb, deal_sum_outbound, payment_sum_outbound, is_horaat_keva, stage, return_code, message, return_message, display_properties, table_page_num, is_error, is_captcha, is_button, site_name, kod_matbea_mekori, esb_services_call FROM accounter_schema.amex_creditcard_transactions WHERE card = ANY($cards!) `; const uploadAmexTransactions = sql<IUploadAmexTransactionsQuery>` INSERT INTO accounter_schema.amex_creditcard_transactions ( specific_date, card_index, deals_inbound, supplier_id, supplier_name, deal_sum_type, payment_sum_sign, purchase_date, full_purchase_date, more_info, horaat_keva, voucher_number, voucher_number_ratz, solek, purchase_date_outbound, full_purchase_date_outbound, currency_id, current_payment_currency, city, supplier_name_outbound, full_supplier_name_outbound, payment_date, full_payment_date, is_show_deals_outbound, adendum, voucher_number_ratz_outbound, is_show_link_for_supplier_details, deal_sum, payment_sum, full_supplier_name_heb, deal_sum_outbound, payment_sum_outbound, is_horaat_keva, stage, return_code, message, return_message, display_properties, table_page_num, is_error, is_captcha, is_button, site_name, client_ip_address, card, charging_date, kod_matbea_mekori, esb_services_call ) VALUES $$transactions( specificDate, cardIndex, dealsInbound, supplierId, supplierName, dealSumType, paymentSumSign, purchaseDate, fullPurchaseDate, moreInfo, horaatKeva, voucherNumber, voucherNumberRatz, solek, purchaseDateOutbound, fullPurchaseDateOutbound, currencyId, currentPaymentCurrency, city, supplierNameOutbound, fullSupplierNameOutbound, paymentDate, fullPaymentDate, isShowDealsOutbound, adendum, voucherNumberRatzOutbound, isShowLinkForSupplierDetails, dealSum, paymentSum, fullSupplierNameHeb, dealSumOutbound, paymentSumOutbound, isHoraatKeva, stage, returnCode, message, returnMessage, displayProperties, tablePageNum, isError, isCaptcha, isButton, siteName, clientIpAddress, card, chargingDate, kodMatbeaMekori, esbServicesCall ) ON CONFLICT ( card, COALESCE(full_purchase_date, full_purchase_date_outbound), full_payment_date, COALESCE(payment_sum, payment_sum_outbound), voucher_number, COALESCE(voucher_number_ratz, voucher_number_ratz_outbound), supplier_id, current_payment_currency, COALESCE(full_supplier_name_heb, full_supplier_name_outbound), more_info, deal_sum ) DO NOTHING RETURNING id, full_purchase_date, supplier_name, payment_sum, card; `; type IsracardAmexKeyShape = { card: number; full_purchase_date?: string | null; full_purchase_date_outbound?: string | null; full_payment_date?: string | null; payment_sum?: string | number | null; payment_sum_outbound?: string | number | null; voucher_number?: number | null; supplier_id?: number | null; current_payment_currency?: string | null; full_supplier_name_heb?: string | null; full_supplier_name_outbound?: string | null; more_info?: string | null; voucher_number_ratz?: number | null; voucher_number_ratz_outbound?: number | null; deal_sum?: string | number | null; }; function isracardAmexConflictKey(row: IsracardAmexKeyShape): string { return JSON.stringify([ row.card, row.full_purchase_date ?? row.full_purchase_date_outbound ?? '', row.full_payment_date ?? '', Number(row.payment_sum ?? row.payment_sum_outbound ?? 0), row.voucher_number ?? 0, row.voucher_number_ratz ?? row.voucher_number_ratz_outbound ?? 0, row.supplier_id ?? 0, row.current_payment_currency ?? '', row.full_supplier_name_heb ?? row.full_supplier_name_outbound ?? '', row.more_info ?? '', Number(row.deal_sum ?? 0), ]); } const ISRACARD_DIFF_FIELDS: Array<{ key: keyof IFetchIsracardByCardsResult; incoming: ( t: | IUploadIsracardTransactionsParams['transactions'][number] | IUploadAmexTransactionsParams['transactions'][number], ) => string | number | boolean | null; }> = [ { key: 'adendum', incoming: t => t.adendum ?? null }, { key: 'city', incoming: t => t.city ?? null }, { key: 'currency_id', incoming: t => t.currencyId ?? null }, { key: 'deal_sum', incoming: t => t.dealSum ?? null }, { key: 'deal_sum_outbound', incoming: t => (t.dealSumOutbound == null ? null : String(t.dealSumOutbound)), }, { key: 'deal_sum_type', incoming: t => t.dealSumType ?? null }, { key: 'deals_inbound', incoming: t => t.dealsInbound ?? null }, { key: 'display_properties', incoming: t => t.displayProperties ?? null }, { key: 'esb_services_call', incoming: t => t.esbServicesCall ?? null }, { key: 'horaat_keva', incoming: t => t.horaatKeva ?? null }, { key: 'is_button', incoming: t => t.isButton ?? null }, { key: 'is_captcha', incoming: t => t.isCaptcha ?? null }, { key: 'is_error', incoming: t => t.isError ?? null }, { key: 'is_horaat_keva', incoming: t => t.isHoraatKeva ?? null }, { key: 'is_show_deals_outbound', incoming: t => t.isShowDealsOutbound ?? null }, { key: 'is_show_link_for_supplier_details', incoming: t => t.isShowLinkForSupplierDetails ?? null, }, { key: 'kod_matbea_mekori', incoming: t => t.kodMatbeaMekori ?? null }, { key: 'message', incoming: t => t.message ?? null }, { key: 'payment_date', incoming: t => t.paymentDate ?? null }, { key: 'payment_sum_sign', incoming: t => t.paymentSumSign ?? null }, { key: 'purchase_date', incoming: t => t.purchaseDate ?? null }, { key: 'purchase_date_outbound', incoming: t => t.purchaseDateOutbound ?? null }, { key: 'return_code', incoming: t => t.returnCode ?? null }, { key: 'return_message', incoming: t => t.returnMessage ?? null }, { key: 'site_name', incoming: t => t.siteName ?? null }, { key: 'solek', incoming: t => t.solek ?? null }, { key: 'specific_date', incoming: t => t.specificDate ?? null }, { key: 'stage', incoming: t => t.stage ?? null }, { key: 'supplier_name', incoming: t => t.supplierName ?? null }, { key: 'supplier_name_outbound', incoming: t => t.supplierNameOutbound ?? null }, { key: 'table_page_num', incoming: t => t.tablePageNum ?? null }, { key: 'voucher_number_ratz', incoming: t => t.voucherNumberRatz ?? null }, { key: 'voucher_number_ratz_outbound', incoming: t => t.voucherNumberRatzOutbound ?? null }, ]; const ISRACARD_AMEX_NUMERIC_FIELDS: (keyof IFetchIsracardByCardsResult)[] = [ 'deal_sum', 'deal_sum_outbound', 'payment_sum', 'payment_sum_outbound', ] as const; function diffIsracardAmexRow<T extends IFetchIsracardByCardsResult | IFetchAmexByCardsResult>( existing: T, incoming: ReturnType< typeof validateIsracardAmexTransactions< T extends IFetchIsracardByCardsResult ? IsracardTransactionInput : AmexTransactionInput > >[number], ): ChangedField[] { const changed: ChangedField[] = []; for (const { key, incoming: getIncoming } of ISRACARD_DIFF_FIELDS) { const isNumberField = ISRACARD_AMEX_NUMERIC_FIELDS.includes(key); const oldValue = formatValue(existing[key], isNumberField); const newValue = formatValue(getIncoming(incoming), isNumberField); const oldStr = oldValue; const newStr = newValue; if (oldStr !== newStr) { changed.push({ field: key, oldValue: oldStr, newValue: newStr }); } } return changed; } @Injectable({ scope: Scope.Operation, global: true, }) export class IsracardAmexScraperIngestionProvider { constructor(private db: TenantAwareDBClient) {} async uploadIsracardTransactions( transactions: readonly IsracardTransactionInput[], ): Promise<ScraperUploadResult> { if (transactions.length === 0) return { inserted: 0, skipped: 0, insertedIds: [], insertedTransactions: [], changedTransactions: [], }; const validated = validateIsracardAmexTransactions(transactions); const cards = [ ...new Set(validated.map(t => t.card).filter((c): c is number => Number.isFinite(c))), ]; const existing = await fetchIsracardByCards.run({ cards: [...cards] }, this.db); const existingByKey = new Map(existing.map(row => [isracardAmexConflictKey(row), row])); const result: IUploadIsracardTransactionsResult[] = await uploadIsracardTransactions.run( { transactions: validated }, this.db, ); const insertedIds = result.map(r => r.id).filter((id): id is string => typeof id === 'string'); const insertedIdSet = new Set(insertedIds); const insertedTransactions: InsertedTransactionSummary[] = result.map(r => ({ id: r.id, date: r.full_purchase_date ?? null, description: r.supplier_name ?? null, amount: r.payment_sum == null ? null : String(r.payment_sum), account: String(r.card), })); const changedTransactions: ChangedTransaction[] = []; for (const t of validated) { if (t.card === null || t.card === undefined) continue; const key = isracardAmexConflictKey({ card: t.card, full_purchase_date: t.fullPurchaseDate ?? null, full_purchase_date_outbound: t.fullPurchaseDateOutbound ?? null, full_payment_date: t.fullPaymentDate ?? null, payment_sum: t.paymentSum !== null && t.paymentSum !== undefined ? String(t.paymentSum) : null, payment_sum_outbound: t.paymentSumOutbound !== null && t.paymentSumOutbound !== undefined ? String(t.paymentSumOutbound) : null, voucher_number: t.voucherNumber ?? null, supplier_id: t.supplierId ?? null, current_payment_currency: t.currentPaymentCurrency ?? null, full_supplier_name_heb: t.fullSupplierNameHeb ?? null, full_supplier_name_outbound: t.fullSupplierNameOutbound ?? null, more_info: t.moreInfo ?? null, }); const existingRow = existingByKey.get(key); if (existingRow && !insertedIdSet.has(existingRow.id)) { const changedFields = diffIsracardAmexRow(existingRow, t); if (changedFields.length > 0) { changedTransactions.push({ id: existingRow.id, changedFields }); } } } return { inserted: insertedIds.length, skipped: transactions.length - insertedIds.length, insertedIds, insertedTransactions, changedTransactions, }; } async uploadAmexTransactions( transactions: readonly AmexTransactionInput[], ): Promise<ScraperUploadResult> { if (transactions.length === 0) return { inserted: 0, skipped: 0, insertedIds: [], insertedTransactions: [], changedTransactions: [], }; const validated = validateIsracardAmexTransactions(transactions); const cards = [ ...new Set(validated.map(t => t.card).filter((c): c is number => Number.isFinite(c))), ]; const existing = await fetchAmexByCards.run({ cards: [...cards] }, this.db); const existingByKey = new Map(existing.map(row => [isracardAmexConflictKey(row), row])); const result: IUploadAmexTransactionsResult[] = await uploadAmexTransactions.run( { transactions: validated }, this.db, ); const insertedIds = result.map(r => r.id).filter((id): id is string => typeof id === 'string'); const insertedIdSet = new Set(insertedIds); const insertedTransactions: InsertedTransactionSummary[] = result.map(r => ({ id: r.id, date: r.full_purchase_date ?? null, description: r.supplier_name ?? null, amount: r.payment_sum == null ? null : String(r.payment_sum), account: String(r.card), })); const changedTransactions: ChangedTransaction[] = []; for (const t of validated) { if (t.card === null || t.card === undefined) continue; const key = isracardAmexConflictKey({ card: t.card, full_purchase_date: t.fullPurchaseDate ?? null, full_purchase_date_outbound: t.fullPurchaseDateOutbound ?? null, full_payment_date: t.fullPaymentDate ?? null, payment_sum: t.paymentSum !== null && t.paymentSum !== undefined ? String(t.paymentSum) : null, payment_sum_outbound: t.paymentSumOutbound !== null && t.paymentSumOutbound !== undefined ? String(t.paymentSumOutbound) : null, voucher_number: t.voucherNumber ?? null, supplier_id: t.supplierId ?? null, current_payment_currency: t.currentPaymentCurrency ?? null, full_supplier_name_heb: t.fullSupplierNameHeb ?? null, full_supplier_name_outbound: t.fullSupplierNameOutbound ?? null, more_info: t.moreInfo ?? null, }); const existingRow = existingByKey.get(key); if (existingRow && !insertedIdSet.has(existingRow.id)) { const changedFields = diffIsracardAmexRow(existingRow, t); if (changedFields.length > 0) { changedTransactions.push({ id: existingRow.id, changedFields }); } } } return { inserted: insertedIds.length, skipped: transactions.length - insertedIds.length, insertedIds, insertedTransactions, changedTransactions, }; } }