PSA/shared/billingClients/defaultContract.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

279 lines
8.3 KiB
TypeScript

import type { Knex } from 'knex';
import { v4 as uuidv4 } from 'uuid';
export const SYSTEM_MANAGED_DEFAULT_CONTRACT_NAME = 'System-managed default contract';
export const SYSTEM_MANAGED_DEFAULT_CONTRACT_DESCRIPTION =
'Created automatically for uncontracted work';
export type EnsureDefaultContractForClientParams = {
tenant: string;
clientId: string;
};
export type EnsureDefaultContractForClientResult = {
contractId: string;
clientContractId: string;
createdContract: boolean;
createdAssignment: boolean;
};
export type EnsureDefaultContractFallbackResult = {
ensured: boolean;
result?: EnsureDefaultContractForClientResult;
};
type DefaultContractEnsureLogOutcome = 'created' | 'reused' | 'skipped_no_billing_configuration';
const logDefaultContractEnsure = (
outcome: DefaultContractEnsureLogOutcome,
payload: {
tenant: string;
clientId: string;
contractId?: string;
clientContractId?: string;
createdContract?: boolean;
createdAssignment?: boolean;
},
): void => {
console.info('[default_contract.ensure]', {
event: 'default_contract.ensure',
outcome,
tenant: payload.tenant,
clientId: payload.clientId,
contractId: payload.contractId,
clientContractId: payload.clientContractId,
createdContract: payload.createdContract ?? false,
createdAssignment: payload.createdAssignment ?? false,
metric: {
name:
outcome === 'created'
? 'default_contract_created'
: outcome === 'reused'
? 'default_contract_reused'
: 'default_contract_skipped_no_billing_configuration',
value: 1,
},
});
};
const isUniqueViolation = (error: unknown): boolean => {
const code = (error as { code?: string } | undefined)?.code;
return code === '23505';
};
const isKnexTransaction = (
knexOrTrx: Knex | Knex.Transaction
): knexOrTrx is Knex.Transaction => {
return (
typeof (knexOrTrx as any).commit === 'function' &&
typeof (knexOrTrx as any).rollback === 'function'
);
};
async function findExistingDefaultContract(
trx: Knex.Transaction,
params: EnsureDefaultContractForClientParams
): Promise<{ contract_id: string; contract_name?: string | null; contract_description?: string | null } | null> {
const rows = await trx('contracts')
.where({
tenant: params.tenant,
owner_client_id: params.clientId,
is_system_managed_default: true,
})
.select('contract_id', 'contract_name', 'contract_description', 'is_template');
const row = rows.find((candidate: { is_template?: boolean | null }) => candidate.is_template !== true);
return row
? {
contract_id: row.contract_id as string,
contract_name: row.contract_name ?? null,
contract_description: row.contract_description ?? null,
}
: null;
}
async function ensureClientContractAssignment(
trx: Knex.Transaction,
params: EnsureDefaultContractForClientParams & { contractId: string }
): Promise<{ clientContractId: string; createdAssignment: boolean }> {
const existing = await trx('client_contracts')
.where({
tenant: params.tenant,
client_id: params.clientId,
contract_id: params.contractId,
})
.select('client_contract_id')
.first();
if (existing?.client_contract_id) {
return { clientContractId: existing.client_contract_id, createdAssignment: false };
}
const now = new Date().toISOString();
const clientContractId = uuidv4();
await trx('client_contracts').insert({
tenant: params.tenant,
client_contract_id: clientContractId,
client_id: params.clientId,
contract_id: params.contractId,
start_date: now,
end_date: null,
is_active: true,
created_at: now,
updated_at: now,
});
return { clientContractId, createdAssignment: true };
}
async function ensureDefaultContractForClientInTransaction(
trx: Knex.Transaction,
params: EnsureDefaultContractForClientParams
): Promise<EnsureDefaultContractForClientResult> {
const client = await trx('clients')
.where({ tenant: params.tenant, client_id: params.clientId })
.select('client_id', 'default_currency_code')
.forUpdate()
.first();
if (!client?.client_id) {
throw new Error(`Client ${params.clientId} not found`);
}
let existing = await findExistingDefaultContract(trx, params);
let createdContract = false;
let contractId = existing?.contract_id;
if (!contractId) {
const now = new Date().toISOString();
const nextContractId = uuidv4();
const currencyCode =
typeof client.default_currency_code === 'string' && client.default_currency_code.trim().length > 0
? client.default_currency_code.trim().toUpperCase()
: 'USD';
try {
await trx('contracts').insert({
tenant: params.tenant,
contract_id: nextContractId,
contract_name: SYSTEM_MANAGED_DEFAULT_CONTRACT_NAME,
contract_description: SYSTEM_MANAGED_DEFAULT_CONTRACT_DESCRIPTION,
billing_frequency: 'monthly',
currency_code: currencyCode,
is_active: true,
status: 'active',
is_template: false,
owner_client_id: params.clientId,
is_system_managed_default: true,
created_at: now,
updated_at: now,
});
contractId = nextContractId;
createdContract = true;
} catch (error) {
if (!isUniqueViolation(error)) {
throw error;
}
existing = await findExistingDefaultContract(trx, params);
if (!existing?.contract_id) {
throw error;
}
contractId = existing.contract_id;
}
}
if (!contractId) {
throw new Error(`Unable to ensure default contract for client ${params.clientId}`);
}
if (
existing?.contract_id === contractId &&
(
existing.contract_name !== SYSTEM_MANAGED_DEFAULT_CONTRACT_NAME ||
(existing.contract_description ?? null) !== SYSTEM_MANAGED_DEFAULT_CONTRACT_DESCRIPTION
)
) {
await trx('contracts')
.where({
tenant: params.tenant,
contract_id: contractId,
})
.update({
contract_name: SYSTEM_MANAGED_DEFAULT_CONTRACT_NAME,
contract_description: SYSTEM_MANAGED_DEFAULT_CONTRACT_DESCRIPTION,
updated_at: new Date().toISOString(),
});
}
const { clientContractId, createdAssignment } = await ensureClientContractAssignment(trx, {
...params,
contractId,
});
return {
contractId,
clientContractId,
createdContract,
createdAssignment,
};
}
export async function ensureDefaultContractForClient(
knexOrTrx: Knex | Knex.Transaction,
params: EnsureDefaultContractForClientParams
): Promise<EnsureDefaultContractForClientResult> {
if (isKnexTransaction(knexOrTrx)) {
const result = await ensureDefaultContractForClientInTransaction(knexOrTrx, params);
logDefaultContractEnsure(
result.createdContract || result.createdAssignment ? 'created' : 'reused',
{
tenant: params.tenant,
clientId: params.clientId,
contractId: result.contractId,
clientContractId: result.clientContractId,
createdContract: result.createdContract,
createdAssignment: result.createdAssignment,
},
);
return result;
}
return (knexOrTrx as Knex).transaction(async (trx) => {
const result = await ensureDefaultContractForClientInTransaction(trx, params);
logDefaultContractEnsure(
result.createdContract || result.createdAssignment ? 'created' : 'reused',
{
tenant: params.tenant,
clientId: params.clientId,
contractId: result.contractId,
clientContractId: result.clientContractId,
createdContract: result.createdContract,
createdAssignment: result.createdAssignment,
},
);
return result;
});
}
export async function ensureDefaultContractForClientIfBillingConfigured(
knexOrTrx: Knex | Knex.Transaction,
params: EnsureDefaultContractForClientParams
): Promise<EnsureDefaultContractFallbackResult> {
const billingSettings = await knexOrTrx('client_billing_settings')
.where({ tenant: params.tenant, client_id: params.clientId })
.select('client_id')
.first();
if (!billingSettings?.client_id) {
logDefaultContractEnsure('skipped_no_billing_configuration', {
tenant: params.tenant,
clientId: params.clientId,
});
return { ensured: false };
}
const result = await ensureDefaultContractForClient(knexOrTrx, params);
return { ensured: true, result };
}