UNPKG

@accounter/server

Version:
243 lines 10.6 kB
import { __decorate, __metadata } from "tslib"; import { Injectable, Scope } from 'graphql-modules'; import { sql } from '@pgtyped/runtime'; import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js'; import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js'; import { DynamicReportProvider } from '../../reports/providers/dynamic-report.provider.js'; const getStepStatuses = sql ` SELECT owner_id, year, step_id, status, notes, evidence_json, updated_at, completed_at FROM accounter_schema.annual_audit_step_status WHERE owner_id = $ownerId AND year = $year; `; const getStepStatus = sql ` SELECT owner_id, year, step_id, status, notes, evidence_json, updated_at, completed_at FROM accounter_schema.annual_audit_step_status WHERE owner_id = $ownerId AND year = $year AND step_id = $stepId; `; const upsertStepStatus = sql ` INSERT INTO accounter_schema.annual_audit_step_status (owner_id, year, step_id, status, notes, completed_at) VALUES ($ownerId, $year, $stepId, $status, $notes, CASE WHEN $status = 'COMPLETED' THEN now() ELSE NULL END) ON CONFLICT (owner_id, year, step_id) DO UPDATE SET status = EXCLUDED.status, notes = EXCLUDED.notes, completed_at = CASE WHEN EXCLUDED.status = 'COMPLETED' AND annual_audit_step_status.completed_at IS NULL THEN now() WHEN EXCLUDED.status != 'COMPLETED' THEN NULL ELSE annual_audit_step_status.completed_at END RETURNING owner_id, year, step_id, status, notes, evidence_json, updated_at, completed_at; `; const upsertStep09Status = sql ` INSERT INTO accounter_schema.annual_audit_step_status (owner_id, year, step_id, status, notes, evidence_json, completed_at) VALUES ($ownerId, $year, '9', 'COMPLETED', NULL, $evidenceJson::jsonb, now()) ON CONFLICT (owner_id, year, step_id) DO UPDATE SET status = 'COMPLETED', notes = EXCLUDED.notes, evidence_json = EXCLUDED.evidence_json, completed_at = COALESCE(annual_audit_step_status.completed_at, now()) RETURNING owner_id, year, step_id, status, notes, evidence_json, updated_at, completed_at; `; const resetStep09ForTemplate = sql ` UPDATE accounter_schema.annual_audit_step_status SET status = 'PENDING', evidence_json = NULL, completed_at = NULL WHERE owner_id = $ownerId AND step_id = '9' AND evidence_json->>'lockedTemplateName' = $templateName; `; const getBalanceCharge = sql ` WITH ledger_by_charge AS ( SELECT count(DISTINCT x.id) AS ledger_count, array_remove(array_agg(DISTINCT x.financial_entity), NULL::uuid) AS ledger_financial_entities, min(x.value_date) AS min_value_date, max(x.value_date) AS max_value_date, min(x.invoice_date) AS min_invoice_date, max(x.invoice_date) AS max_invoice_date, x.charge_id FROM ( SELECT lr.charge_id, lr.id, lr.value_date, lr.invoice_date, unnest( ARRAY[ lr.credit_entity1, lr.credit_entity2, lr.debit_entity1, lr.debit_entity2 ] ) AS financial_entity FROM accounter_schema.ledger_records lr WHERE lr.charge_id IS NOT NULL AND lr.owner_id = $ownerId ) x GROUP BY x.charge_id ) SELECT c.id FROM accounter_schema.charges c LEFT JOIN ledger_by_charge l ON l.charge_id = c.id WHERE c.owner_id = $ownerId AND type = 'FINANCIAL' AND l.min_value_date <= make_date($year, 12, 31) AND l.max_value_date >= make_date($year, 12, 31) AND user_description ILIKE '%balance%'`; let AnnualAuditProvider = class AnnualAuditProvider { adminContextProvider; dynamicReportProvider; db; constructor(adminContextProvider, dynamicReportProvider, db) { this.adminContextProvider = adminContextProvider; this.dynamicReportProvider = dynamicReportProvider; this.db = db; } async getOpeningBalanceStatus(ownerId, year) { const adminContext = await this.adminContextProvider.getVerifiedAdminContext(); const { initialAccounterYear, dateEstablished } = adminContext; // Guard: missing configuration if (initialAccounterYear == null || dateEstablished == null) { return { id: `${ownerId}:${year}`, userType: 'BLOCKED', balanceChargeId: null, derivedStatus: 'BLOCKED', errorMessage: 'Business configuration is incomplete. Please set initialAccounterYear and dateEstablished in Settings > Admin Context.', }; } const establishedYear = new Date(dateEstablished).getFullYear(); const userType = this.classifyUserType(year, initialAccounterYear, establishedYear); if (userType === 'ERROR') { return { id: `${ownerId}:${year}`, userType: 'ERROR', balanceChargeId: null, derivedStatus: 'BLOCKED', errorMessage: `Report year ${year} precedes the first year tracked in Accounter (${initialAccounterYear}). Verify the business configuration (initialAccounterYear, dateEstablished) and try again.`, }; } if (userType === 'NEW') { return { id: `${ownerId}:${year}`, userType: 'NEW', balanceChargeId: null, derivedStatus: 'COMPLETED', }; } if (userType === 'CONTINUING') { // Prior-year data is already in-system; no DB checks needed. // derivedStatus is always PENDING — accountant approval is required. return { id: `${ownerId}:${year}`, userType: 'CONTINUING', balanceChargeId: null, derivedStatus: 'PENDING', }; } // MIGRATING: check for balance charge const balanceChargeId = await this.checkBalanceChargeExists(ownerId, year - 1); const derivedStatus = this.computeMigratingDerivedStatus(balanceChargeId); return { id: `${ownerId}:${year}`, userType, balanceChargeId, derivedStatus, }; } classifyUserType(reportYear, initialAccounterYear, establishedYear) { if (reportYear < initialAccounterYear) { return 'ERROR'; } if (reportYear === initialAccounterYear && establishedYear === initialAccounterYear) { return 'NEW'; } if (reportYear === initialAccounterYear && establishedYear < initialAccounterYear) { return 'MIGRATING'; } return 'CONTINUING'; } computeMigratingDerivedStatus(balanceChargeId) { // COMPLETED is never set automatically; accountant approval is required. if (balanceChargeId) { return 'IN_PROGRESS'; } return 'PENDING'; } async checkBalanceChargeExists(ownerId, year) { const result = await getBalanceCharge.run({ ownerId, year }, this.db); return result[0]?.id || null; } // ─── Persistence: manual step statuses ─────────────────────────────────── async getStepStatuses(ownerId, year) { const rows = await getStepStatuses.run({ ownerId, year }, this.db); return rows.map(r => this.mapStepStatusRow(r)); } async upsertStepStatus(input) { const { ownerId, year, stepId, status, notes } = input; const [row] = await upsertStepStatus.run({ ownerId, year, stepId, status, notes: notes ?? null }, this.db); if (!row) { throw new Error(`Failed to upsert annual audit step status for step ${stepId}`); } return this.mapStepStatusRow(row); } async setStep09Status(input) { const { ownerId, year, templateName } = input; // Find currently locked template for this step (if any) so we can unlock it const existingRows = await getStepStatus.run({ ownerId, year, stepId: '9' }, this.db); const existing = existingRows[0]; if (existing?.evidence_json) { const prevEvidence = existing.evidence_json; const prevTemplateName = prevEvidence.lockedTemplateName; if (prevTemplateName && prevTemplateName !== templateName) { // Unlock the previously locked template (best-effort; don't fail if already gone) try { await this.dynamicReportProvider.unlockTemplate({ name: prevTemplateName, ownerId }); } catch { // Template may have been deleted; ignore } // Reset step 09 for any other audit years still referencing the unlocked template await this.resetStep09ForTemplate(ownerId, prevTemplateName); } } // Lock the newly selected template await this.dynamicReportProvider.lockTemplate({ name: templateName, ownerId }); // Upsert step 09 as COMPLETED with the locked template name as evidence const evidenceJson = JSON.stringify({ lockedTemplateName: templateName }); const [row] = await upsertStep09Status.run({ ownerId, year, evidenceJson }, this.db); if (!row) { throw new Error('Failed to upsert annual audit step 09 status'); } return this.mapStepStatusRow(row); } async resetStep09ForTemplate(ownerId, templateName) { await resetStep09ForTemplate.run({ ownerId, templateName }, this.db); } mapStepStatusRow(r) { return { id: `${r.owner_id}:${r.year}:${r.step_id}`, ownerId: r.owner_id, year: r.year, stepId: r.step_id, status: r.status, notes: r.notes ?? null, evidence: r.evidence_json == null ? null : JSON.stringify(r.evidence_json), updatedAt: r.updated_at, completedAt: r.completed_at ?? null, }; } }; AnnualAuditProvider = __decorate([ Injectable({ scope: Scope.Operation, global: true, }), __metadata("design:paramtypes", [AdminContextProvider, DynamicReportProvider, TenantAwareDBClient]) ], AnnualAuditProvider); export { AnnualAuditProvider }; //# sourceMappingURL=annual-audit.provider.js.map