UNPKG

@accounter/server

Version:
245 lines 12.8 kB
import { __decorate, __metadata } from "tslib"; import { GraphQLError } from 'graphql'; import { Injectable, Scope } from 'graphql-modules'; import { sql } from '@pgtyped/runtime'; import { makeUUID } from '../../../shared/helpers/deterministic-uuid.js'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js'; import { Auth0ManagementProvider } from '../../auth/providers/auth0-management.provider.js'; import { BusinessUsersProvider } from '../../auth/providers/business-users.provider.js'; import { InvitationsProvider } from '../../auth/providers/invitations.provider.js'; import { SuperAdminProvider } from '../../auth/providers/super-admin.provider.js'; import { AdminBusinessesProvider } from '../../financial-entities/providers/admin-businesses.provider.js'; import { EntityEnsureProvider } from '../../financial-entities/providers/entity-ensure.provider.js'; const setRlsContext = sql ` SELECT set_config('app.current_business_id', $businessId, true); `; const setSelfOwner = sql ` UPDATE accounter_schema.financial_entities SET owner_id = $id WHERE id = $id; `; const getBootstrapBusiness = sql ` 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 = $id; `; const getBootstrapContext = sql ` SELECT * FROM accounter_schema.user_context WHERE owner_id = $ownerId; `; const expireActiveInvitations = sql ` UPDATE accounter_schema.invitations SET expires_at = NOW(), accepted_at = NOW() WHERE business_id = $businessId AND lower(email) = lower($email) AND accepted_at IS NULL AND expires_at > NOW(); `; let AdminOnboardingProvider = class AdminOnboardingProvider { db; entityEnsure; adminContextProvider; businessUsersProvider; adminBusinessesProvider; auth0Management; invitationsProvider; superAdminProvider; constructor(db, entityEnsure, adminContextProvider, businessUsersProvider, adminBusinessesProvider, auth0Management, invitationsProvider, superAdminProvider) { this.db = db; this.entityEnsure = entityEnsure; this.adminContextProvider = adminContextProvider; this.businessUsersProvider = businessUsersProvider; this.adminBusinessesProvider = adminBusinessesProvider; this.auth0Management = auth0Management; this.invitationsProvider = invitationsProvider; this.superAdminProvider = superAdminProvider; } async bootstrapNewClient(input) { const superAdmin = await this.superAdminProvider.requireSuperAdmin(); return this.db.transaction(async (client) => { const { businessName, countryCode, locality, dateEstablished, initialAccounterYear, ownerEmail, ownerRole, } = input; // 0. Ensure country reference data (no RLS on countries table) await this.entityEnsure.ensureCountry(countryCode, countryCode, client); // 1. Determine the new admin entity's deterministic ID so we can set RLS context before inserting const adminEntityId = makeUUID('business', businessName); // Set RLS context — needed for all inserts via the tenant_isolation policy await setRlsContext.run({ businessId: adminEntityId }, client); // Insert financial_entity with owner_id=NULL first (allow_bootstrap_root policy allows // id = current_business_id). Then insert businesses, then update owner_id to self. await this.entityEnsure.ensureFinancialEntity({ id: adminEntityId, name: businessName, type: 'business', }, client); await this.entityEnsure.ensureBusinessForEntity(adminEntityId, { ownerId: adminEntityId, country: countryCode, }, client); // Update owner_id to self (allow_bootstrap_owner_update RLS policy permits this) await setSelfOwner.run({ id: adminEntityId }, client); // Register as an admin-managed business (required for business_users FK) await this.adminBusinessesProvider.insertAdminBusiness(adminEntityId, client); // 2. Authority businesses const authorityBusinessIds = {}; for (const name of ['VAT', 'Tax', 'Social Security']) { const { id } = await this.entityEnsure.ensureFinancialEntity({ name, type: 'business', ownerId: adminEntityId, }, client); authorityBusinessIds[name] = id; await this.entityEnsure.ensureBusinessForEntity(id, { isDocumentsOptional: true, ownerId: adminEntityId, }, client); } // 3. Authority tax categories const authorityTaxCategoryIds = {}; for (const name of ['Input Vat', 'Output Vat', 'Property Output Vat', 'Tax Expenses']) { const { id } = await this.entityEnsure.ensureFinancialEntity({ name, type: 'tax_category', ownerId: adminEntityId, }, client); authorityTaxCategoryIds[name] = id; await this.entityEnsure.ensureTaxCategoryForEntity(id, { ownerId: adminEntityId }, client); } // 4. General tax categories const generalTaxCategoryIds = {}; for (const name of [ 'DEFAULT (missing)', 'Exchange Rates', 'Income Exchange Rates', 'Exchange Revaluation', 'Fee', 'General Fee', 'Fine', 'Untaxable Gifts', 'Balance Cancellation', 'Development Foreign', 'Development Local', 'Salary Excess Expenses', ]) { const { id } = await this.entityEnsure.ensureFinancialEntity({ name, type: 'tax_category', ownerId: adminEntityId, }, client); generalTaxCategoryIds[name] = id; await this.entityEnsure.ensureTaxCategoryForEntity(id, { ownerId: adminEntityId }, client); } // 5. Cross-year tax categories const crossYearTaxCategoryIds = {}; for (const name of [ 'Expenses to Pay', 'Expenses in Advance', 'Income to Collect', 'Income in Advance', ]) { const { id } = await this.entityEnsure.ensureFinancialEntity({ name, type: 'tax_category', ownerId: adminEntityId, }, client); crossYearTaxCategoryIds[name] = id; await this.entityEnsure.ensureTaxCategoryForEntity(id, { ownerId: adminEntityId }, client); } // 6. Insert user_context — dynamic column list, cannot use PgTyped const contextCols = { owner_id: adminEntityId, default_local_currency: 'ILS', default_fiat_currency_for_crypto_conversions: 'USD', locality: locality ?? 'ISR', default_tax_category_id: generalTaxCategoryIds['DEFAULT (missing)'], vat_business_id: authorityBusinessIds['VAT'], input_vat_tax_category_id: authorityTaxCategoryIds['Input Vat'], output_vat_tax_category_id: authorityTaxCategoryIds['Output Vat'], property_output_vat_tax_category_id: authorityTaxCategoryIds['Property Output Vat'], tax_business_id: authorityBusinessIds['Tax'], tax_expenses_tax_category_id: authorityTaxCategoryIds['Tax Expenses'], social_security_business_id: authorityBusinessIds['Social Security'], exchange_rate_tax_category_id: generalTaxCategoryIds['Exchange Rates'], income_exchange_rate_tax_category_id: generalTaxCategoryIds['Income Exchange Rates'], exchange_rate_revaluation_tax_category_id: generalTaxCategoryIds['Exchange Revaluation'], fee_tax_category_id: generalTaxCategoryIds['Fee'], general_fee_tax_category_id: generalTaxCategoryIds['General Fee'], fine_tax_category_id: generalTaxCategoryIds['Fine'], untaxable_gifts_tax_category_id: generalTaxCategoryIds['Untaxable Gifts'], balance_cancellation_tax_category_id: generalTaxCategoryIds['Balance Cancellation'], development_foreign_tax_category_id: generalTaxCategoryIds['Development Foreign'], development_local_tax_category_id: generalTaxCategoryIds['Development Local'], expenses_to_pay_tax_category_id: crossYearTaxCategoryIds['Expenses to Pay'], expenses_in_advance_tax_category_id: crossYearTaxCategoryIds['Expenses in Advance'], income_to_collect_tax_category_id: crossYearTaxCategoryIds['Income to Collect'], income_in_advance_tax_category_id: crossYearTaxCategoryIds['Income in Advance'], salary_excess_expenses_tax_category_id: generalTaxCategoryIds['Salary Excess Expenses'], ...(dateEstablished ? { date_established: dateEstablished } : {}), ...(initialAccounterYear ? { initial_accounter_year: initialAccounterYear } : {}), }; const columns = Object.keys(contextCols).join(', '); const placeholders = Object.keys(contextCols) .map((_, i) => `$${i + 1}`) .join(', '); await client.query(`INSERT INTO accounter_schema.user_context (${columns}) VALUES (${placeholders}) ON CONFLICT (owner_id) DO NOTHING`, Object.values(contextCols)); // 7. Create Auth0 user (blocked) + invitation // Expire any standing invitations so insertInvitation can create a fresh one await expireActiveInvitations.run({ businessId: adminEntityId, email: ownerEmail }, client); const auth0UserId = await this.auth0Management.createBlockedUser(ownerEmail); await this.businessUsersProvider.insertBusinessUser({ userId: superAdmin.userId, auth0UserId: superAdmin.auth0UserId, roleId: ownerRole, ownerId: adminEntityId, }); const { token: invitationToken } = await this.invitationsProvider.insertInvitation({ email: ownerEmail, roleId: ownerRole, auth0UserId, invitedByUserId: superAdmin.userId, ownerId: adminEntityId, }); // 8. Fetch business and adminContext while RLS context is still set to adminEntityId const [businessResults, contextResults] = await Promise.all([ getBootstrapBusiness.run({ id: adminEntityId }, client), getBootstrapContext.run({ ownerId: adminEntityId }, client), ]); const business = businessResults[0]; if (!business) { throw new GraphQLError('Admin business not found after bootstrap', { extensions: { code: 'INTERNAL_SERVER_ERROR' }, }); } const rawContext = contextResults[0]; if (!rawContext) { throw new GraphQLError('Admin context not found after bootstrap', { extensions: { code: 'INTERNAL_SERVER_ERROR' }, }); } const adminContext = this.adminContextProvider.normalizeContext(rawContext); return { adminEntityId, invitationToken, business, adminContext, }; }); } }; AdminOnboardingProvider = __decorate([ Injectable({ scope: Scope.Operation }), __metadata("design:paramtypes", [TenantAwareDBClient, EntityEnsureProvider, AdminContextProvider, BusinessUsersProvider, AdminBusinessesProvider, Auth0ManagementProvider, InvitationsProvider, SuperAdminProvider]) ], AdminOnboardingProvider); export { AdminOnboardingProvider }; //# sourceMappingURL=admin-onboarding.provider.js.map