@accounter/server
Version:
Accounter GraphQL server
243 lines • 10.6 kB
JavaScript
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