@accounter/server
Version:
Accounter GraphQL server
363 lines (341 loc) • 11.5 kB
text/typescript
import DataLoader from 'dataloader';
import { Injectable, Scope } from 'graphql-modules';
// eslint-disable-next-line no-restricted-imports
import type { PoolClient } from 'pg';
import { sql } from '@pgtyped/runtime';
import { reassureOwnerIdExists } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import type {
IGetAllBusinessesQuery,
IGetAllBusinessesResult,
IGetBusinessByEmailQuery,
IGetBusinessesByIdsQuery,
IInsertBusinessesParams,
IInsertBusinessesQuery,
IReplaceBusinessesQuery,
IUpdateBusinessParams,
IUpdateBusinessQuery,
} from '../types.js';
const getBusinessesByIds = sql<IGetBusinessesByIdsQuery>`
SELECT fe.*, b.vat_number, b.hebrew_name, b.address, b.address_hebrew, b.email, b.website, b.phone_number, b.country, b.no_invoices_required, b.suggestion_data, b.can_settle_with_receipt, b.exempt_dealer, b.optional_vat, b.pcn874_record_type_override, b.city, b.zip_code
FROM accounter_schema.businesses b
INNER JOIN accounter_schema.financial_entities fe
USING (id)
WHERE b.id IN $$ids;`;
const getAllBusinesses = sql<IGetAllBusinessesQuery>`
SELECT fe.*, b.vat_number, b.hebrew_name, b.address, b.address_hebrew, b.email, b.website, b.phone_number, b.country, b.no_invoices_required, b.suggestion_data, b.can_settle_with_receipt, b.exempt_dealer, b.optional_vat, b.pcn874_record_type_override, b.city, b.zip_code
FROM accounter_schema.businesses b
INNER JOIN accounter_schema.financial_entities fe
USING (id);`;
const getBusinessByEmail = sql<IGetBusinessByEmailQuery>`
SELECT fe.*, b.vat_number, b.hebrew_name, b.address, b.address_hebrew, b.email, b.website, b.phone_number, b.country, b.no_invoices_required, b.suggestion_data, b.can_settle_with_receipt, b.exempt_dealer, b.optional_vat, b.pcn874_record_type_override, b.city, b.zip_code
FROM accounter_schema.businesses b
INNER JOIN accounter_schema.financial_entities fe
USING (id)
WHERE
suggestion_data->'emails' ? $email::text
LIMIT 1;`;
const updateBusiness = sql<IUpdateBusinessQuery>`
UPDATE accounter_schema.businesses
SET
vat_number = COALESCE(
$vatNumber,
vat_number
),
hebrew_name = COALESCE(
$hebrewName,
hebrew_name
),
address = COALESCE(
$address,
address
),
city = COALESCE(
$city,
city
),
zip_code = COALESCE(
$zipCode,
zip_code
),
address_hebrew = COALESCE(
$addressHebrew,
address_hebrew
),
email = COALESCE(
$email,
email
),
website = COALESCE(
$website,
website
),
phone_number = COALESCE(
$phoneNumber,
phone_number
),
country = COALESCE(
$country,
country
),
no_invoices_required = COALESCE(
$isDocumentsOptional,
no_invoices_required
),
suggestion_data = COALESCE(
$suggestionData,
suggestion_data
),
can_settle_with_receipt = COALESCE(
$isReceiptEnough,
can_settle_with_receipt
),
exempt_dealer = COALESCE(
$exemptDealer,
exempt_dealer
),
optional_vat = COALESCE(
$optionalVat,
optional_vat
),
pcn874_record_type_override = COALESCE(
$pcn874RecordTypeOverride,
pcn874_record_type_override
)
WHERE
id = $businessId
RETURNING *;
`;
const insertBusinesses = sql<IInsertBusinessesQuery>`
INSERT INTO accounter_schema.businesses (id, hebrew_name, address, city, zip_code, email, website, phone_number, vat_number, exempt_dealer, suggestion_data, optional_vat, country, pcn874_record_type_override, can_settle_with_receipt, no_invoices_required, owner_id)
VALUES $$businesses(id, hebrewName, address, city, zipCode, email, website, phoneNumber, governmentId, exemptDealer, suggestions, optionalVat, country, pcn874RecordTypeOverride, isReceiptEnough, isDocumentsOptional, ownerId)
RETURNING *;`;
const replaceBusinesses = sql<IReplaceBusinessesQuery>`
WITH ledger_debit1 AS (
UPDATE accounter_schema.ledger_records
SET debit_entity1 = $targetBusinessId
WHERE debit_entity1 = $businessIdToReplace
RETURNING id
),
ledger_debit2 AS (
UPDATE accounter_schema.ledger_records
SET debit_entity2 = $targetBusinessId
WHERE debit_entity2 = $businessIdToReplace
RETURNING id
),
ledger_credit1 AS (
UPDATE accounter_schema.ledger_records
SET credit_entity1 = $targetBusinessId
WHERE credit_entity1 = $businessIdToReplace
RETURNING id
),
ledger_credit2 AS (
UPDATE accounter_schema.ledger_records
SET credit_entity2 = $targetBusinessId
WHERE credit_entity2 = $businessIdToReplace
RETURNING id
),
documents_debtor AS (
UPDATE accounter_schema.documents
SET debtor_id = $targetBusinessId
WHERE debtor_id = $businessIdToReplace
RETURNING id
),
documents_creditor AS (
UPDATE accounter_schema.documents
SET creditor_id = $targetBusinessId
WHERE creditor_id = $businessIdToReplace
RETURNING id
),
employees AS (
UPDATE accounter_schema.employees
SET business_id = $targetBusinessId
WHERE business_id = $businessIdToReplace
RETURNING business_id
),
funds AS (
UPDATE accounter_schema.pension_funds
SET id = $targetBusinessId
WHERE id = $businessIdToReplace
RETURNING id
),
business_trips_attendees AS (
UPDATE accounter_schema.business_trips_attendees
SET attendee_business_id = $targetBusinessId
WHERE attendee_business_id = $businessIdToReplace
RETURNING business_trip_id
),
business_trips_employee_payments AS (
UPDATE accounter_schema.business_trips_employee_payments
SET employee_business_id = $targetBusinessId
WHERE employee_business_id = $businessIdToReplace
RETURNING id
),
charge_unbalanced_ledger_businesses AS (
UPDATE accounter_schema.charge_unbalanced_ledger_businesses
SET business_id = $targetBusinessId
WHERE business_id = $businessIdToReplace
RETURNING charge_id
),
charge_balance_cancellation AS (
UPDATE accounter_schema.charge_balance_cancellation
SET business_id = $targetBusinessId
WHERE business_id = $businessIdToReplace
RETURNING charge_id
),
dividends AS (
UPDATE accounter_schema.dividends
SET business_id = $targetBusinessId
WHERE business_id = $businessIdToReplace
RETURNING id
),
corporate_tax_variables as (
UPDATE accounter_schema.corporate_tax_variables
SET corporate_id = $targetBusinessId
WHERE corporate_id = $businessIdToReplace
RETURNING date
),
clients AS (
UPDATE accounter_schema.clients
SET business_id = $targetBusinessId
WHERE business_id = $businessIdToReplace
RETURNING (integrations->>'greenInvoiceId')::uuid
)
UPDATE accounter_schema.transactions
SET business_id = $targetBusinessId
WHERE business_id = $businessIdToReplace
RETURNING id;
`;
({
scope: Scope.Operation,
global: true,
})
export class BusinessesProvider {
constructor(
private db: TenantAwareDBClient,
private adminContextProvider: AdminContextProvider,
) {}
private async batchBusinessesByIds(ids: readonly string[]) {
const uniqueIds = [...new Set(ids)];
const businesses = await getBusinessesByIds.run(
{
ids: uniqueIds,
},
this.db,
);
return ids.map(id => businesses.find(fe => fe.id === id));
}
public getBusinessByIdLoader = new DataLoader((ids: readonly string[]) =>
this.batchBusinessesByIds(ids),
);
private allBusinessesCache: Promise<IGetAllBusinessesResult[]> | null = null;
public getAllBusinesses() {
if (this.allBusinessesCache) {
return this.allBusinessesCache;
}
this.allBusinessesCache = getAllBusinesses.run(undefined, this.db).then(businesses => {
businesses.map(business => {
this.getBusinessByIdLoader.prime(business.id, business);
});
return businesses;
});
return this.allBusinessesCache;
}
public getBusinessByEmail(email: string) {
return getBusinessByEmail.run({ email }, this.db).then(res => {
if (res?.length) {
res.map(business => {
this.getBusinessByIdLoader.prime(business.id, business);
});
return res[0];
}
return null;
});
}
public async updateBusiness(
params: Omit<IUpdateBusinessParams, 'businessId'> & { businessId: string },
) {
if (params.businessId) await this.invalidateBusinessById(params.businessId);
return updateBusiness.run(params, this.db);
}
private async batchInsertBusinesses(
newBusinesses: readonly IInsertBusinessesParams['businesses'][number][],
) {
await Promise.all(
newBusinesses.map(async nb => {
if (nb.id) {
await this.invalidateBusinessById(nb.id);
}
}),
);
const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
const businessesWithOwnerId = newBusinesses.map(business =>
reassureOwnerIdExists(business, ownerId),
);
const businesses = await insertBusinesses.run(
{
businesses: businessesWithOwnerId,
},
this.db,
);
return newBusinesses.map(nb => businesses.find(b => b.id === nb.id) ?? null);
}
public insertBusinessLoader = new DataLoader(
(businesses: readonly IInsertBusinessesParams['businesses'][number][]) =>
this.batchInsertBusinesses(businesses),
{
cache: false,
},
);
public async insertBusiness(
newBusiness: IInsertBusinessesParams['businesses'][number],
client?: PoolClient,
) {
if (newBusiness.id) {
await this.invalidateBusinessById(newBusiness.id);
}
const ownerId =
newBusiness.ownerId ?? (await this.adminContextProvider.getVerifiedAdminContext()).ownerId;
const businessWithOwnerId = reassureOwnerIdExists(newBusiness, ownerId);
const businesses = await insertBusinesses.run(
{
businesses: [businessWithOwnerId],
},
client ?? this.db,
);
return businesses[0];
}
public async replaceBusiness(targetBusinessId: string, businessIdToReplace: string) {
const [businessToReplace, business] = await Promise.all([
this.getBusinessByIdLoader.load(businessIdToReplace),
this.getBusinessByIdLoader.load(targetBusinessId),
]);
if (!businessToReplace) {
throw new Error(`Business with id ${businessIdToReplace} not found`);
}
if (!business) {
throw new Error(`Business with id ${targetBusinessId} not found`);
}
this.invalidateBusinessById(businessIdToReplace);
this.invalidateBusinessById(targetBusinessId);
// convert transactions, ledger, documents, employees, funds, business trips attendees, business trips employee payments, charge unbalanced ledger businesses, charge balance cancellation, dividends, corporate tax variables, businesses green invoice match
await replaceBusinesses.run(
{
targetBusinessId,
businessIdToReplace,
},
this.db,
);
// TODO: convert when owner: financial entities, charges, business-tax-category, ledger
// TODO: should convert business tax category matches?
}
public async invalidateBusinessById(businessId: string) {
this.getBusinessByIdLoader.clear(businessId);
this.allBusinessesCache = null;
}
public clearCache() {
this.getBusinessByIdLoader.clearAll();
this.allBusinessesCache = null;
}
}