UNPKG

@accounter/server

Version:
589 lines (569 loc) • 16.2 kB
import { Injectable, Scope } from 'graphql-modules'; import { sql } from '@pgtyped/runtime'; import type { CalTransactionInput, ChangedField, ChangedTransaction, CurrencyRateInput, DiscountTransactionInput, InsertedTransactionSummary, MaxTransactionInput, ScraperUploadResult, } from '../../../__generated__/types.js'; import { dateToTimelessDateString } from '../../../shared/helpers/index.js'; import { TimelessDateString } from '../../../shared/types/index.js'; import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js'; import { FiatExchangeProvider } from '../../exchange-rates/providers/fiat-exchange.provider.js'; import { IGetExchangeRatesByDatesResult } from '../../exchange-rates/types.js'; import { formatValue } from '../helpers/utils.helper.js'; import { validateCalTransactions, validateDiscountTransactions, validateMaxTransactions, validateRates, } from '../helpers/validators.helper.js'; import type { IUploadCalTransactionsQuery, IUploadCurrencyRatesParams, IUploadCurrencyRatesQuery, IUploadDiscountTransactionsQuery, IUploadMaxTransactionsQuery, } from '../types.js'; const uploadCalTransactions = sql<IUploadCalTransactionsQuery>` INSERT INTO accounter_schema.cal_creditcard_transactions ( card, trn_int_id, trn_numaretor, merchant_name, trn_purchase_date, trn_amt, trn_currency_symbol, trn_type, trn_type_code, deb_crd_date, amt_before_conv_and_index, deb_crd_currency_symbol, merchant_address, merchant_phone_no, branch_code_desc, trans_card_present_ind, cur_payment_num, num_of_payments, token_ind, wallet_provider_code, wallet_provider_desc, token_number_part4, cash_account_trn_amt, charge_external_to_card_comment, refund_ind, is_immediate_comment_ind, is_immediate_hhk_ind, is_margarita, is_spread_paymenst_abroad, trn_exac_way, debit_spread_ind, on_going_transactions_comment, early_payment_ind, merchant_id, crd_ext_id_num_type_code, trans_source, is_abroad_transaction ) VALUES $$transactions( card, trnIntId, trnNumaretor, merchantName, trnPurchaseDate, trnAmt, trnCurrencySymbol, trnType, trnTypeCode, debCrdDate, amtBeforeConvAndIndex, debCrdCurrencySymbol, merchantAddress, merchantPhoneNo, branchCodeDesc, transCardPresentInd, curPaymentNum, numOfPayments, tokenInd, walletProviderCode, walletProviderDesc, tokenNumberPart4, cashAccountTrnAmt, chargeExternalToCardComment, refundInd, isImmediateCommentInd, isImmediateHhkInd, isMargarita, isSpreadPaymenstAbroad, trnExacWay, debitSpreadInd, onGoingTransactionsComment, earlyPaymentInd, merchantId, crdExtIdNumTypeCode, transSource, isAbroadTransaction ) ON CONFLICT (trn_int_id) DO NOTHING RETURNING id, trn_purchase_date, merchant_name, trn_amt, card; `; const uploadDiscountTransactions = sql<IUploadDiscountTransactionsQuery>` INSERT INTO accounter_schema.bank_discount_transactions ( operation_date, value_date, operation_code, operation_description, operation_description2, operation_description3, operation_branch, operation_bank, account_number, channel, channel_name, check_number, institute_code, operation_amount, balance_after_operation, operation_number, branch_treasury_number, urn, operation_details_service_name, commission_channel_code, commission_channel_name, commission_type_name, business_day_date, event_name, category_code, category_desc_code, category_description, operation_description_to_display, operation_order, is_last_seen ) VALUES $$transactions( operationDate, valueDate, operationCode, operationDescription, operationDescription2, operationDescription3, operationBranch, operationBank, accountNumber, channel, channelName, checkNumber, instituteCode, operationAmount, balanceAfterOperation, operationNumber, branchTreasuryNumber, urn, operationDetailsServiceName, commissionChannelCode, commissionChannelName, commissionTypeName, businessDayDate, eventName, categoryCode, categoryDescCode, categoryDescription, operationDescriptionToDisplay, operationOrder, isLastSeen ) ON CONFLICT (urn, account_number) WHERE urn IS NOT NULL AND account_number IS NOT NULL DO NOTHING RETURNING id, operation_date, operation_description, operation_amount, account_number; `; const uploadMaxTransactions = sql<IUploadMaxTransactionsQuery>` INSERT INTO accounter_schema.max_creditcard_transactions ( actual_payment_amount, arn, card_index, category_id, comments, deal_data_acq, deal_data_adjustment_amount, deal_data_adjustment_type, deal_data_amount, deal_data_amount_ils, deal_data_amount_left, deal_data_arn, deal_data_authorization_number, deal_data_card_name, deal_data_card_token, deal_data_commission_vat, deal_data_direct_exchange, deal_data_exchange_commission_amount, deal_data_exchange_commission_maam, deal_data_exchange_commission_type, deal_data_exchange_direct, deal_data_exchange_rate, deal_data_index_rate_base, deal_data_index_rate_pmt, deal_data_interest_amount, deal_data_is_allowed_spread_with_benefit, deal_data_issuer_currency, deal_data_issuer_exchange_rate, deal_data_original_term, deal_data_percent_maam, deal_data_plan, deal_data_pos_entry_emv, deal_data_processing_date, deal_data_purchase_amount, deal_data_purchase_time, deal_data_ref_nbr, deal_data_show_cancel_debit, deal_data_show_spread, deal_data_show_spread_benefit_button, deal_data_show_spread_button, deal_data_show_spread_for_leumi, deal_data_tdm_card_token, deal_data_tdm_transaction_type, deal_data_transaction_type, deal_data_txn_code, deal_data_user_name, deal_data_withdrawal_commission_amount, discount_key_amount, discount_key_rec_type, ethoca_ind, funds_transfer_comment, funds_transfer_receiver_or_transfer, is_register_ch, is_spreading_autorization_allowed, issuer_id, merchant_address, merchant_coordinates, merchant_max_phone, merchant, merchant_commercial_name, merchant_number, merchant_phone, merchant_tax_id, merchant_name, original_amount, original_currency, payment_currency, payment_date, plan_name, plan_type_id, promotion_amount, promotion_club, promotion_type, purchase_date, receipt_p_d_f, ref_index, runtime_reference_internal_id, runtime_reference_type, runtime_reference_id, short_card_number, spread_transaction_by_campain_ind, spread_transaction_by_campain_number, table_type, tag, uid, up_sale_for_transaction_result, user_index ) VALUES $$transactions( actualPaymentAmount, arn, cardIndex, categoryId, comments, dealDataAcq, dealDataAdjustmentAmount, dealDataAdjustmentType, dealDataAmount, dealDataAmountIls, dealDataAmountLeft, dealDataArn, dealDataAuthorizationNumber, dealDataCardName, dealDataCardToken, dealDataCommissionVat, dealDataDirectExchange, dealDataExchangeCommissionAmount, dealDataExchangeCommissionMaam, dealDataExchangeCommissionType, dealDataExchangeDirect, dealDataExchangeRate, dealDataIndexRateBase, dealDataIndexRatePmt, dealDataInterestAmount, dealDataIsAllowedSpreadWithBenefit, dealDataIssuerCurrency, dealDataIssuerExchangeRate, dealDataOriginalTerm, dealDataPercentMaam, dealDataPlan, dealDataPosEntryEmv, dealDataProcessingDate, dealDataPurchaseAmount, dealDataPurchaseTime, dealDataRefNbr, dealDataShowCancelDebit, dealDataShowSpread, dealDataShowSpreadBenefitButton, dealDataShowSpreadButton, dealDataShowSpreadForLeumi, dealDataTdmCardToken, dealDataTdmTransactionType, dealDataTransactionType, dealDataTxnCode, dealDataUserName, dealDataWithdrawalCommissionAmount, discountKeyAmount, discountKeyRecType, ethocaInd, fundsTransferComment, fundsTransferReceiverOrTransfer, isRegisterCh, isSpreadingAutorizationAllowed, issuerId, merchantAddress, merchantCoordinates, merchantMaxPhone, merchant, merchantCommercialName, merchantNumber, merchantPhone, merchantTaxId, merchantName, originalAmount, originalCurrency, paymentCurrency, paymentDate, planName, planTypeId, promotionAmount, promotionClub, promotionType, purchaseDate, receiptPDF, refIndex, runtimeReferenceInternalId, runtimeReferenceType, runtimeReferenceId, shortCardNumber, spreadTransactionByCampainInd, spreadTransactionByCampainNumber, tableType, tag, uid, upSaleForTransactionResult, userIndex ) ON CONFLICT (uid, arn, purchase_date, payment_date, original_amount) DO NOTHING RETURNING id, purchase_date, merchant_name, actual_payment_amount, card_index; `; const uploadCurrencyRates = sql<IUploadCurrencyRatesQuery>` INSERT INTO accounter_schema.exchange_rates ( exchange_date, usd, eur, gbp, cad, jpy, aud, sek ) VALUES $$rates( exchangeDate, usd, eur, gbp, cad, jpy, aud, sek ) ON CONFLICT (exchange_date) DO NOTHING RETURNING exchange_date, usd, eur, gbp, cad, jpy, aud, sek; `; function diffExchangeRatesRow<T extends IGetExchangeRatesByDatesResult>( existing: T, incoming: IUploadCurrencyRatesParams['rates'][number], ): ChangedField[] { const changed: ChangedField[] = []; for (const currency of ['aud', 'cad', 'eur', 'gbp', 'jpy', 'sek', 'usd'] as const) { const oldValue = formatValue(existing[currency], true); const newValue = formatValue(incoming[currency], true); const oldStr = oldValue; const newStr = newValue; if (oldStr !== newStr) { changed.push({ field: currency, oldValue: String(oldStr), newValue: String(newStr) }); } } return changed; } @Injectable({ scope: Scope.Operation, global: true, }) export class ScraperIngestionProvider { constructor( private db: TenantAwareDBClient, private exchangeRates: FiatExchangeProvider, ) {} async uploadCalTransactions( transactions: readonly CalTransactionInput[], ): Promise<ScraperUploadResult> { if (transactions.length === 0) return { inserted: 0, skipped: 0, insertedIds: [], insertedTransactions: [], changedTransactions: [], }; const result = await uploadCalTransactions.run( { transactions: validateCalTransactions(transactions) }, this.db, ); const insertedIds = result.map(r => r.id).filter((id): id is string => typeof id === 'string'); const insertedTransactions: InsertedTransactionSummary[] = result.map(r => ({ id: r.id, date: r.trn_purchase_date ?? null, description: r.merchant_name ?? null, amount: r.trn_amt == null ? null : String(r.trn_amt), account: String(r.card), })); return { inserted: insertedIds.length, skipped: transactions.length - insertedIds.length, insertedIds, insertedTransactions, changedTransactions: [], }; } async uploadDiscountTransactions( transactions: readonly DiscountTransactionInput[], ): Promise<ScraperUploadResult> { if (transactions.length === 0) return { inserted: 0, skipped: 0, insertedIds: [], insertedTransactions: [], changedTransactions: [], }; const result = await uploadDiscountTransactions.run( { transactions: validateDiscountTransactions(transactions) }, this.db, ); const insertedIds = result.map(r => r.id).filter((id): id is string => typeof id === 'string'); const insertedTransactions: InsertedTransactionSummary[] = result.map(r => ({ id: r.id, date: r.operation_date ?? null, description: r.operation_description ?? null, amount: r.operation_amount ?? null, account: r.account_number ?? null, })); return { inserted: insertedIds.length, skipped: transactions.length - insertedIds.length, insertedIds, insertedTransactions, changedTransactions: [], }; } async uploadMaxTransactions( transactions: readonly MaxTransactionInput[], ): Promise<ScraperUploadResult> { if (transactions.length === 0) return { inserted: 0, skipped: 0, insertedIds: [], insertedTransactions: [], changedTransactions: [], }; const result = await uploadMaxTransactions.run( { transactions: validateMaxTransactions(transactions) }, this.db, ); const insertedIds = result.map(r => r.id).filter((id): id is string => typeof id === 'string'); const insertedTransactions: InsertedTransactionSummary[] = result.map(r => ({ id: r.id, date: r.purchase_date ? dateToTimelessDateString(r.purchase_date) : null, description: r.merchant_name ?? null, amount: r.actual_payment_amount == null ? null : String(r.actual_payment_amount), account: String(r.card_index), })); return { inserted: insertedIds.length, skipped: transactions.length - insertedIds.length, insertedIds, insertedTransactions, changedTransactions: [], }; } async uploadCurrencyRates(rates: readonly CurrencyRateInput[]): Promise<ScraperUploadResult> { if (rates.length === 0) return { inserted: 0, skipped: 0, insertedIds: [], insertedTransactions: [], changedTransactions: [], }; const validated = validateRates(rates); const existing = await this.exchangeRates.getExchangeRatesByDatesLoader .loadMany(rates.map(r => new Date(r.exchangeDate))) .then(res => res.filter((r): r is IGetExchangeRatesByDatesResult => r !== null && !(r instanceof Error)), ); const existingByDate = new Map( existing.map(row => [ row.exchange_date ? dateToTimelessDateString(row.exchange_date) : null, row, ]), ); const result = await uploadCurrencyRates.run({ rates: validated }, this.db); const insertedDates = result .map(r => (r.exchange_date ? dateToTimelessDateString(r.exchange_date) : undefined)) .filter((date): date is TimelessDateString => !!date); const insertedDatesSet = new Set(insertedDates); const insertedRates: InsertedTransactionSummary[] = result.map(r => { const date = dateToTimelessDateString(r.exchange_date!); const description = (['usd', 'eur', 'gbp', 'cad', 'jpy', 'aud', 'sek'] as const) .filter(c => r[c] != null) .map(c => `${c.toUpperCase()}=${r[c]}`) .join(', '); return { id: date ?? undefined, date, description, amount: undefined, account: undefined, }; }); const changedRates: ChangedTransaction[] = []; for (const r of validated) { if (!r.exchangeDate) continue; const date = dateToTimelessDateString(new Date(r.exchangeDate)); const existingRow = existingByDate.get(date); if ( existingRow?.exchange_date && !insertedDatesSet.has(dateToTimelessDateString(existingRow.exchange_date)) ) { const changedFields = diffExchangeRatesRow(existingRow, r); if (changedFields.length > 0) { changedRates.push({ id: date, changedFields }); } } } return { inserted: insertedDates.length, skipped: rates.length - insertedDates.length, insertedIds: insertedDates, insertedTransactions: insertedRates, changedTransactions: changedRates, }; } }