Files
picpeak/backend/src/routes/adminDeals.js
T
Luca 03fa3d8296 fix(flags): close CRM/accounting feature-gating gaps from the audit
A sweep of every CRM/accounting toggle found surfaces still reachable
with their flag OFF. Adds a shared requireFeatureFlag middleware (the two
existing per-file copies predate it) and closes the gaps:

- Hours logging: only createEntry checked the flag — edit/delete/bill and
  the list/summary routes were permission-only. Gate all six
  /hour-entries routes on the hoursLogging master so a disabled feature
  can't be read, mutated, or invoiced via a direct API hit.
- Installment plans: PUT /deals/:uuid/installment-plan mutates invoices
  but wasn't bills-gated; add requireFeatureFlag('bills').
- Customer invoice PDF: /invoices/:id/pdf lacked the feature_bills check
  the list + quotes routes have. Also fixes the quotes-PDF gate, which
  read req.customer.feature_quotes (never populated → silent no-op).
- Customer contracts: /contracts + /contracts/:id/pdf were gated by
  neither the master nor a per-customer column.

Per-customer contracts override (the missing counterpart):
- Migration 131 adds customer_accounts.feature_contracts, default TRUE so
  existing customers keep their Contracts tab (preserve-visuals).
- Effective resolver now contractsMaster AND feature_contracts; admin
  detail page gains the toggle; service/validator/serializer wired.

Cleanups:
- Drop stale `taxReport` from the sidebar's Clients-reveal list (Tax moved
  to Accounting); add the missing `projects` so it mirrors the context
  derivation.
- SettingsPage tab-snap effect now depends on flags.accounting.
- Fix stale taxReport "forced off when bills off" comment (it's accounting).
2026-06-16 13:12:26 +02:00

88 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Admin → deals lineage endpoint.
*
* One UUID per customer engagement spans every quote, contract, and
* invoice (migration 140). This route exposes the union: given a
* deal_uuid, return every related document so the frontend's
* DocumentLineageCard can render the full chain with a single query
* instead of walking the legacy point-to-point FKs in JS.
*
* Read-only. The same `customers.view` permission used elsewhere for
* lineage display is the gate here — anyone who can read a quote or
* invoice detail page can read its deal lineage.
*
* Sibling routes (`/api/admin/quotes/:id/lineage`,
* `/api/admin/contracts/:id/lineage`, `/api/admin/invoices/:id/lineage`)
* also exist as conveniences so the frontend doesn't have to fetch
* the deal_uuid first; they resolve and delegate to the same service.
*/
const express = require('express');
const { param, body } = require('express-validator');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { requireFeatureFlag } = require('../middleware/requireFeatureFlag');
const { handleAsync, validateRequest, successResponse } = require('../utils/routeHelpers');
const dealsService = require('../services/dealsService');
const invoiceService = require('../services/invoiceService');
const { db } = require('../database/db');
const router = express.Router();
router.use(adminAuth);
router.get(
'/:uuid/documents',
requirePermission('customers.view'),
// UUID v4 format check — adminCalendar uses a similar pattern.
// Length window 3236 covers both hyphenated and non-hyphenated
// forms; the service does the actual lookup.
[param('uuid').isString().isLength({ min: 32, max: 36 })],
handleAsync(async (req, res) => {
validateRequest(req);
const result = await dealsService.getDealDocuments(req.params.uuid);
return successResponse(res, result);
}),
);
/**
* Atomically reshape an installment plan after siblings have spawned.
* Delegates to invoiceService.updateInstallmentPlan inside a transaction.
* See that service function for the guard/reuse/grow/trim semantics.
*
* 400 — invalid input (validator or service-side percent sum / unknown
* trigger / single-invoice deal).
* 404 — deal_uuid owns no invoices.
* 409 — at least one sibling is past `scheduled`/`pending_delivery`, or
* the deal contains a Storno.
*/
router.put(
'/:uuid/installment-plan',
// Mutates invoices — gate on the bills flag like every other invoice
// write path, so installment plans can't be reshaped with Bills off.
requireFeatureFlag('bills', 'BILLS_DISABLED'),
requirePermission('bills.manage'),
[
param('uuid').isString().isLength({ min: 32, max: 36 }),
body('installments').isArray({ min: 1 }),
body('installments.*.percent').isFloat({ min: 0, max: 100 }),
body('installments.*.trigger').isIn([
'quote_accepted', 'before_event', 'after_event', 'after_delivery', 'fixed_date',
]),
body('installments.*.offset_days').isInt(),
body('installments.*.label').optional({ values: 'falsy' }).isString().isLength({ max: 200 }),
],
handleAsync(async (req, res) => {
validateRequest(req);
const adminId = req.admin?.id;
const result = await db.transaction((trx) => invoiceService.updateInstallmentPlan({
trx,
dealUuid: req.params.uuid,
installments: req.body.installments,
adminId,
}));
return successResponse(res, result);
}),
);
module.exports = router;