UNPKG

@accounter/server

Version:
744 lines (717 loc) 26.6 kB
import { GraphQLError } from 'graphql'; import type { ChargeResolvers, Maybe, Resolver, ResolversParentTypes, ResolversTypes, } from '../../../../__generated__/types.js'; import { UUID_REGEX } from '../../../../shared/constants.js'; import { ChargeTypeEnum } from '../../../../shared/enums.js'; import { errorSimplifier } from '../../../../shared/errors.js'; import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js'; import { ScopeProvider } from '../../../auth/providers/scope.provider.js'; import { suggestionDataSchema } from '../../../financial-entities/helpers/business-suggestion-data-schema.helper.js'; import { BusinessesProvider } from '../../../financial-entities/providers/businesses.provider.js'; import { ChargeTagsProvider } from '../../../tags/providers/charge-tags.provider.js'; import { TagsProvider } from '../../../tags/providers/tags.provider.js'; import { IGetTagsByIDsResult } from '../../../tags/types.js'; import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js'; import { getChargeType } from '../../helpers/charge-type.js'; import { calculateTotalAmount, getChargeBusinesses } from '../../helpers/common.helper.js'; import { ChargesProvider } from '../../providers/charges.provider.js'; import type { ChargesModule } from '../../types.js'; import { missingConversionInfoSuggestions } from './conversion-suggeestions.resolver.js'; export type Suggestion = Awaited<ResolversTypes['ChargeSuggestions']>; const missingInfoSuggestions: Resolver< Maybe<Suggestion>, ResolversParentTypes['Charge'], GraphQLModules.Context > = async (DbCharge, _, context, __) => { try { const { injector } = context; const { financialAccounts: { poalimBusinessId, etherScanBusinessId, krakenBusinessId, etanaBusinessId, }, defaultLocalCurrency, } = await injector.get(AdminContextProvider).getVerifiedAdminContext(); const [chargeType, tags] = await Promise.all([ getChargeType(DbCharge, injector), injector.get(ChargeTagsProvider).getTagsByChargeIDLoader.load(DbCharge.id), ]); // if all required fields are filled, no need for suggestions if (!!tags.length && !!DbCharge.user_description?.trim()) { return null; } if (chargeType === ChargeTypeEnum.Conversion) { return missingConversionInfoSuggestions(DbCharge, _, context, __); } const [formattedAmount, { allBusinessIds, mainBusinessId }] = await Promise.all([ calculateTotalAmount(DbCharge.id, injector, defaultLocalCurrency), getChargeBusinesses(DbCharge.id, injector), ]); const chargeAmount = formattedAmount?.raw ?? 0; // if charge has a businesses, use it's suggestion data if (mainBusinessId) { const business = await injector .get(BusinessesProvider) .getBusinessByIdLoader.load(mainBusinessId); if (business?.suggestion_data) { const { data: suggestionData, error, success, } = suggestionDataSchema.safeParse(business.suggestion_data); if (!success) { console.error('Failed to parse suggestion data for business', { businessId: business.id, error, }); throw new GraphQLError('Failed to parse suggestion data for business'); } return { description: suggestionData.description, tags: await Promise.all( (suggestionData.tags ?? []).map(tag => UUID_REGEX.test(tag) ? injector.get(TagsProvider).getTagByIDLoader.load(tag) : injector.get(TagsProvider).getTagByNameLoader.load(tag), ), ).then(tags => tags.filter(Boolean) as IGetTagsByIDsResult[]), }; } } if (allBusinessIds.length > 1) { const isKrakenIncluded = krakenBusinessId && allBusinessIds.includes(krakenBusinessId); const isEtherscanIncluded = etherScanBusinessId && allBusinessIds.includes(etherScanBusinessId); const isEtanaIncluded = etanaBusinessId && allBusinessIds.includes(etanaBusinessId); const isPoalimIncluded = poalimBusinessId && allBusinessIds.includes(poalimBusinessId); if (isKrakenIncluded && isEtherscanIncluded) { return { description: 'Etherscan to Kraken transfer', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('financial') .then(res => (res ? [res] : [])), }; } if (isKrakenIncluded && isEtanaIncluded) { return { description: 'Kraken to Etana transfer', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('financial') .then(res => (res ? [res] : [])), }; } if (isPoalimIncluded && isEtanaIncluded) { return { description: 'Etana to Poalim transfer', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('financial') .then(res => (res ? [res] : [])), }; } } const allBusinesses = await injector.get(BusinessesProvider).getAllBusinesses(); const suggestions: Record<string, Suggestion> = {}; for (const business of allBusinesses) { if (!business.suggestion_data) continue; const { data: suggestionData, error, success, } = suggestionDataSchema.safeParse(business.suggestion_data); if (!success) { console.error('Failed to parse suggestion data for business', { businessId: business.id, error, }); continue; } if (business.id in allBusinessIds) { return { description: suggestionData.description, tags: await Promise.all( (suggestionData.tags ?? []).map(tag => UUID_REGEX.test(tag) ? injector.get(TagsProvider).getTagByIDLoader.load(tag) : injector.get(TagsProvider).getTagByNameLoader.load(tag), ), ).then(tags => tags.filter(Boolean) as IGetTagsByIDsResult[]), }; } if (!suggestionData.phrases) continue; for (const phrase of suggestionData.phrases) { suggestions[phrase] = { description: suggestionData.description, tags: suggestionData.tags ? await Promise.all( suggestionData.tags.map(tag => UUID_REGEX.test(tag) ? injector.get(TagsProvider).getTagByIDLoader.load(tag) : injector.get(TagsProvider).getTagByNameLoader.load(tag), ), ).then(tags => tags.filter(Boolean) as IGetTagsByIDsResult[]) : [], }; } } const transactions = await injector .get(TransactionsProvider) .transactionsByChargeIDLoader.load(DbCharge.id); const description = transactions.map(t => t.source_description).join(' '); for (const [phrase, suggestion] of Object.entries(suggestions)) { if (Array.isArray(phrase) && new RegExp(phrase.join('|')).test(description)) { return suggestion; } if (description.includes(phrase)) { return suggestion; } } if ( description.includes('ע\' העברת מט"ח') || (description.includes('העברת מט"ח') && Math.abs(chargeAmount) < 400) || (description.includes('מטח') && Math.abs(chargeAmount) < 400) || description.includes('F.C.COM') || description.includes('ע.מפעולות-ישיר') || description.includes('ריבית חובה') || description.includes('FEE') ) { const sourceTransaction = transactions.length === 0 ? 'Missing' : transactions.length === 1 ? transactions[0].source_reference : `['${transactions.map(t => t.source_reference).join("','")}']`; return { tags: await injector .get(TagsProvider) .getTagByNameLoader.load('financial') .then(res => (res ? [res] : [])), description: `Fees for source transaction=${sourceTransaction}`, }; } if (description.includes('דותן שמחה') || description.includes('שמחה דותן')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `${previousMonth}/2022 Salary`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('גולדשטין אורי')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `${previousMonth}/2022 Salary`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('גרדוש')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `${previousMonth}/2022 Salary`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('תובל')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `${previousMonth}/2022 Salary`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('מנורה מבטחים פנס')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `Pension ${previousMonth}/2022`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('פניקס אקסלנס')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `Training Fund ${previousMonth}/2022`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('מיטב דש גמל ופנס')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `Pension ${previousMonth}/2022`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('מגדל מקפת')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `Pension ${previousMonth}/2022`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('מגדל השתלמות')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `Training Fund ${previousMonth}/2022`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('ביטוח לאומי')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `Social Security Deductions for Salaries ${previousMonth}/2022`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('LANCE GLOBAL')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: 'long' }); return { description: `The Guild Enterprise Support - ${previousMonth} 2022`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if ( (description.includes('העברת מט"ח') && (description.includes('fbv') || description.includes('fv'))) || description.includes('kamil kisiela') ) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `Software Development and Consulting ${previousMonth}/23`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('slava')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit' }); return { description: `Web Development Services ${previousMonth}/23`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('COURIER PLUS INC')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: 'long' }); return { description: `GraphQL Hive Enterprise License - ${previousMonth} 2023`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('GOBRANDS')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: 'long' }); return { description: `GraphQL Hive Enterprise License - ${previousMonth} 2023`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('MEDIC FIRST AI')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: 'long' }); return { description: `GraphQL Hive Enterprise License - ${previousMonth} 2023`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('מס הכנסה')) { const flag = description.includes('מס הכנסה ני'); const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), description: flag ? `Tax for employees for ${previousMonth}/2022` : `Advance Tax for ${previousMonth}/2022`, }; } if (description.includes('המכס ומעמ-גביי תשלום') || description.includes('CUSTOM + V.A.T')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit', }); return { description: `VAT for ${previousMonth}/2022`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('חניון')) { return { description: 'Parking', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('transportation') .then(res => (res ? [res] : [])), }; } if (description.includes('deel')) { return { description: 'Laurin Salary', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('GITHUB')) { const suggested = { description: 'GitHub Actions', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; if (chargeAmount <= -2000) { suggested.description = 'Monthly Sponsor for Benjie, Code-Hex, hayes'; } else if (chargeAmount <= -1000) { suggested.description = 'Monthly Sponsor for Andarist, warrenday'; } else { suggested.description = 'GitHub Actions'; } return suggested; } if (chargeAmount === -4329) { return { description: 'Office rent', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('APPLE COM BILL/ITUNES.COM')) { const flag = chargeAmount === -109.9; return { taxCategory: 'אתר', beneficiaaries: [], // NOTE: used to be ' ' description: flag ? 'LinkedIn' : 'Apple Services', tags: await injector .get(TagsProvider) .getTagByNameLoader.load(flag ? 'business' : 'computer') .then(res => (res ? [res] : [])), }; } if ( description.includes('ע\' העברת מט"ח') || (description.includes('העברת מט"ח') && Math.abs(chargeAmount) < 400) || (description.includes('מטח') && Math.abs(chargeAmount) < 400) || description.includes('F.C.COM') || description.includes('ע.מפעולות-ישיר') || description.includes('ריבית חובה') || description.includes('FEE') ) { const description = transactions.length ? `['${transactions.map(t => t.source_reference).join("','")}']` : 'Missing'; //NOTE: multiple suggestions business return { tags: await injector .get(TagsProvider) .getTagByNameLoader.load('financial') .then(res => (res ? [res] : [])), description, }; } if (description.includes('ריבית זכות')) { //NOTE: multiple suggestions business return { description: 'Interest fees on Euro plus', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('financial') .then(res => (res ? [res] : [])), }; } if (description.includes('פועלים- דמי כרטיס')) { //NOTE: multiple suggestions business return { description: 'Bank creditcard fees', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('financial') .then(res => (res ? [res] : [])), }; } if (description.includes('אריה קריסטל')) { //NOTE: multiple suggestions business return { description: 'Water bill for 04-2022', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('house') .then(res => (res ? [res] : [])), }; } if (description.includes('aleksandra')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit' }); return { description: `Software Consulting Fees (${previousMonth}/2023)`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (description.includes('denelop')) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit' }); return { description: `Software Development and Consulting ${previousMonth}/2023`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (chargeAmount === -12_000) { const current = new Date(); current.setMonth(current.getMonth() - 1); const previousMonth = current.toLocaleString('default', { month: '2-digit' }); return { description: `${previousMonth}/2022`, tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } if (chargeAmount === -600) { return { description: 'Matic Zavadlal - April 2021', tags: await injector .get(TagsProvider) .getTagByNameLoader.load('business') .then(res => (res ? [res] : [])), }; } return null; } catch (error) { throw errorSimplifier('Unexpected error fetching missing info suggestions', error); } }; const commonChargeFields: ChargesModule.ChargeResolvers = { missingInfoSuggestions: missingInfoSuggestions as ChargeResolvers['missingInfoSuggestions'], }; export const chargeSuggestionsResolvers: ChargesModule.Resolvers = { Query: { similarCharges: async ( _, { chargeId, withMissingTags = false, withMissingDescription = false, descriptionDifferentThan, tagsDifferentThan, }, { injector }, ) => { if (!chargeId?.trim()) { throw new GraphQLError('Charge ID is required'); } try { const [mainCharge, { allBusinessIds, mainBusinessId }] = await Promise.all([ injector .get(ChargesProvider) .getChargeByIdLoader.load(chargeId) .catch(e => { console.error('Error fetching charge', { chargeId, error: e }); throw new GraphQLError('Error fetching charge'); }), getChargeBusinesses(chargeId, injector), ]); if (!mainCharge) { throw new GraphQLError(`Charge not found: ${chargeId}`); } const similarCharges = await injector .get(ChargesProvider) .getSimilarCharges({ businessId: mainBusinessId, businessArray: allBusinessIds, withMissingTags, withMissingDescription, tagsDifferentThan: tagsDifferentThan ? [...tagsDifferentThan] : undefined, descriptionDifferentThan, ownerId: mainCharge.owner_id, }) .catch(e => { console.error('Error fetching similar charges:', { chargeId, businessId: mainBusinessId, businessArray: allBusinessIds, error: e.message, }); throw new GraphQLError('Error fetching similar charges'); }); return similarCharges; } catch (error) { throw errorSimplifier('Unexpected error fetching similar charges', error); } }, similarChargesByBusiness: async ( _, { businessId, ownerId: ownerIdInput, tagsDifferentThan, descriptionDifferentThan }, { injector }, ) => { if (!businessId?.trim()) { throw new GraphQLError('Business ID is required'); } try { // Validate an explicit owner against the read scope (throws if out of // scope); otherwise default to the request's primary business. const trimmedOwnerId = ownerIdInput?.trim(); let ownerId: string; if (trimmedOwnerId) { await injector.get(ScopeProvider).getReadScope([trimmedOwnerId]); ownerId = trimmedOwnerId; } else { ownerId = (await injector.get(AdminContextProvider).getVerifiedAdminContext()).ownerId; } const similarCharges = await injector .get(ChargesProvider) .getSimilarCharges({ businessId, tagsDifferentThan: tagsDifferentThan ? [...tagsDifferentThan] : undefined, descriptionDifferentThan, ownerId, }) .catch(e => { console.error('Error fetching similar charges:', { businessId, ownerId, error: e.message, }); throw new GraphQLError('Error fetching similar charges by business'); }); return similarCharges; } catch (error) { throw errorSimplifier('Unexpected error fetching similar charges by business', error); } }, }, CommonCharge: commonChargeFields, FinancialCharge: commonChargeFields, ConversionCharge: commonChargeFields, SalaryCharge: commonChargeFields, InternalTransferCharge: commonChargeFields, DividendCharge: commonChargeFields, BusinessTripCharge: commonChargeFields, MonthlyVatCharge: commonChargeFields, BankDepositCharge: commonChargeFields, ForeignSecuritiesCharge: commonChargeFields, CreditcardBankCharge: commonChargeFields, };