diff --git a/apps/server/package.json b/apps/server/package.json index b57086a4..e50dcc8b 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@erp/factuges-server", - "version": "0.7.8", + "version": "0.8.2", "private": true, "scripts": { "build": "tsup src/index.ts --config tsup.config.ts", diff --git a/apps/web/package.json b/apps/web/package.json index f2f8626d..5ce104e5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@erp/factuges-web", "private": true, - "version": "0.7.8", + "version": "0.8.2", "type": "module", "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", diff --git a/modules/auth/package.json b/modules/auth/package.json index e280de97..27455f0f 100644 --- a/modules/auth/package.json +++ b/modules/auth/package.json @@ -1,6 +1,6 @@ { "name": "@erp/auth", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/catalogs/package.json b/modules/catalogs/package.json index 18accab3..f3efad40b 100644 --- a/modules/catalogs/package.json +++ b/modules/catalogs/package.json @@ -1,7 +1,7 @@ { "name": "@erp/catalogs", "description": "Catalogs module", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/core/package.json b/modules/core/package.json index ee7a9af3..2c1c584e 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -1,6 +1,6 @@ { "name": "@erp/core", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index b1f14cae..39f0a100 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customer-invoices", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts index cbc82264..df05c53d 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts @@ -8,6 +8,7 @@ import { InvoicePaymentMethod, InvoiceStatus, IssuedInvoiceItem, + IssuedInvoiceRecipient, IssuedInvoiceTax, IssuedInvoiceTaxes, type Proforma, @@ -29,6 +30,11 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic public toCreateProps(source: ProformaIssueReadModel): Result { const { proforma } = source; + const recipientResult = this.resolveRecipient(proforma); + if (recipientResult.isFailure) { + return Result.fail(recipientResult.error); + } + const itemsResult = this.resolveItems(proforma); if (itemsResult.isFailure) { @@ -72,7 +78,7 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic paymentMethod: paymentResult.data, customerId: proforma.customerId, - recipient: proforma.recipient.getOrUndefined()!, + recipient: recipientResult.data, items: itemsResult.data, @@ -99,7 +105,31 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic totalAmount: proformaTotals.totalAmount, verifactu: Maybe.none(), + } satisfies IIssuedInvoiceCreateProps); + } + + private resolveRecipient(proforma: Proforma): Result { + const recipient = proforma.recipient; + + if (recipient.isNone()) { + return Result.fail(new Error("Recipient is required")); + } + + const recipientValue = recipient.unwrap(); + + const newRecipient = IssuedInvoiceRecipient.create({ + name: recipientValue.name, + tin: recipientValue.tin.unwrap(), + + street: recipientValue.street.unwrap(), + street2: recipientValue.street2, + city: recipientValue.city.unwrap(), + postalCode: recipientValue.postalCode.unwrap(), + province: recipientValue.province, + country: recipientValue.country, }); + + return newRecipient; } private resolveItems(proforma: Proforma): Result { diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts index a8fa8332..60304aff 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts @@ -1,5 +1,5 @@ import type { ISnapshotBuilder } from "@erp/core/api"; -import { DomainValidationError, maybeToEmptyString, maybeToNullable } from "@repo/rdx-ddd"; +import { DomainValidationError, maybeToNullable } from "@repo/rdx-ddd"; import type { IssuedInvoice } from "../../../../domain"; @@ -22,12 +22,12 @@ export class IssuedInvoiceRecipientFullSnapshotBuilder return { id: invoice.customerId.toString(), name: recipient.name.toString(), - tin: maybeToEmptyString(recipient.tin, (v) => v.toString()), - street: maybeToNullable(recipient.street, (v) => v.toString()), + tin: recipient.tin.toString(), + street: recipient.street.toString(), street2: maybeToNullable(recipient.street2, (v) => v.toString()), - city: maybeToNullable(recipient.city, (v) => v.toString()), + city: recipient.city.toString(), province: maybeToNullable(recipient.province, (v) => v.toString()), - postal_code: maybeToNullable(recipient.postalCode, (v) => v.toString()), + postal_code: recipient.postalCode.toString(), country: maybeToNullable(recipient.country, (v) => v.toString()), }; } diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/issued-invoice-recipient.vo.ts b/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/issued-invoice-recipient.vo.ts index 7d70a030..4f41ff59 100644 --- a/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/issued-invoice-recipient.vo.ts +++ b/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/issued-invoice-recipient.vo.ts @@ -14,10 +14,10 @@ import { type Maybe, Result } from "@repo/rdx-utils"; export type IssuedInvoiceRecipientProps = { name: Name; tin: TINNumber; - street: Maybe; + street: Street; street2: Maybe; - city: Maybe; - postalCode: Maybe; + city: City; + postalCode: PostalCode; province: Maybe; country: Maybe; }; @@ -56,7 +56,7 @@ export class IssuedInvoiceRecipient extends ValueObject { + get street(): Street { return this.props.street; } @@ -64,11 +64,11 @@ export class IssuedInvoiceRecipient extends ValueObject { + get city(): City { return this.props.city; } - get postalCode(): Maybe { + get postalCode(): PostalCode { return this.props.postalCode; } @@ -92,10 +92,10 @@ export class IssuedInvoiceRecipient extends ValueObject value.toString()), + street: this.street.toString(), street2: maybeToNullable(this.street2, (value) => value.toString()), - city: maybeToNullable(this.city, (value) => value.toString()), - postal_code: maybeToNullable(this.postalCode, (value) => value.toString()), + city: this.city.toString(), + postal_code: this.postalCode.toString(), province: maybeToNullable(this.province, (value) => value.toString()), country: maybeToNullable(this.country, (value) => value.toString()), }; diff --git a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts index fac4a7c4..21e5b0c9 100644 --- a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts +++ b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts @@ -314,7 +314,7 @@ export class Proforma extends AggregateRoot implements IP return Result.ok(false); } - if (!canManuallyTransitionProformaStatus({ currentStatus, nextStatus })) { + if (!canManuallyTransitionProformaStatus({ currentStatus, nextStatus, proforma: this })) { return Result.fail( new InvalidProformaTransitionError( currentStatus.toPrimitive(), @@ -392,6 +392,26 @@ export class Proforma extends AggregateRoot implements IP ); } + const recipientValue = this.recipient.unwrap(); + + if ( + !recipientValue.name || + recipientValue.tin.isNone() || + recipientValue.street.isNone() || + recipientValue.city.isNone() || + recipientValue.postalCode.isNone() || + recipientValue.province.isNone() || + recipientValue.country.isNone() + ) { + return Result.fail( + new DomainValidationError( + "MISSING_RECIPIENT", + "recipient", + "Recipient is required to issue the proforma" + ) + ); + } + if (this.items.size() === 0) { return Result.fail( new DomainValidationError( diff --git a/modules/customer-invoices/src/api/domain/proformas/services/proforma-manual-status-transitions.ts b/modules/customer-invoices/src/api/domain/proformas/services/proforma-manual-status-transitions.ts index 06100499..e60e7a85 100644 --- a/modules/customer-invoices/src/api/domain/proformas/services/proforma-manual-status-transitions.ts +++ b/modules/customer-invoices/src/api/domain/proformas/services/proforma-manual-status-transitions.ts @@ -1,4 +1,4 @@ -import { INVOICE_STATUS, type InvoiceStatus } from "../.."; +import { INVOICE_STATUS, type InvoiceStatus, type Proforma } from "../.."; /** * Transiciones manuales permitidas para una proforma. @@ -17,11 +17,38 @@ const PROFORMA_MANUAL_STATUS_TRANSITIONS: Readonly< }; export function canManuallyTransitionProformaStatus(params: { + proforma: Proforma; currentStatus: InvoiceStatus; nextStatus: InvoiceStatus; }): boolean { const current = params.currentStatus.toPrimitive() as INVOICE_STATUS; const next = params.nextStatus.toPrimitive() as INVOICE_STATUS; - return PROFORMA_MANUAL_STATUS_TRANSITIONS[current].includes(next); + let possible = PROFORMA_MANUAL_STATUS_TRANSITIONS[current].includes(next); + + // Validación adicional: si la transición es diferente a 'draft', debe existir + // un destinatario obligatoriamente con todos sus datos rellenos. + + if (next !== INVOICE_STATUS.DRAFT) { + const { recipient } = params.proforma; + if (recipient.isNone()) { + possible = false; + } else { + const recipientValue = recipient.unwrap(); + + if ( + !recipientValue.name || + recipientValue.tin.isNone() || + recipientValue.street.isNone() || + recipientValue.city.isNone() || + recipientValue.postalCode.isNone() || + recipientValue.province.isNone() || + recipientValue.country.isNone() + ) { + possible = false; + } + } + } + + return possible; } diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts index d64d815d..6cb8c36f 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts @@ -47,11 +47,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper { //const customerTin = extractOrPushError(TINNumber.create(_tin!), "customer_tin", errors); const customerTin = extractOrPushError(TINNumber.create(_tin), "customer_tin", errors); - const customerStreet = extractOrPushError( - maybeFromNullableResult(_street, (value) => Street.create(value)), - "customer_street", - errors - ); + const customerStreet = extractOrPushError(Street.create(_street), "customer_street", errors); const customerStreet2 = extractOrPushError( maybeFromNullableResult(_street2, (value) => Street.create(value)), @@ -59,11 +55,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper { errors ); - const customerCity = extractOrPushError( - maybeFromNullableResult(_city, (value) => City.create(value)), - "customer_city", - errors - ); + const customerCity = extractOrPushError(City.create(_city), "customer_city", errors); const customerProvince = extractOrPushError( maybeFromNullableResult(_province, (value) => Province.create(value)), @@ -72,7 +64,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper { ); const customerPostalCode = extractOrPushError( - maybeFromNullableResult(_postal_code, (value) => PostalCode.create(value)), + PostalCode.create(_postal_code), "customer_postal_code", errors ); @@ -125,11 +117,11 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper { return { customer_tin: recipient.tin.toString(), customer_name: recipient.name.toPrimitive(), - customer_street: maybeToNullable(recipient.street, (v) => v.toPrimitive()), + customer_street: recipient.street.toPrimitive(), customer_street2: maybeToNullable(recipient.street2, (v) => v.toPrimitive()), - customer_city: maybeToNullable(recipient.city, (v) => v.toPrimitive()), + customer_city: recipient.city.toPrimitive(), customer_province: maybeToNullable(recipient.province, (v) => v.toPrimitive()), - customer_postal_code: maybeToNullable(recipient.postalCode, (v) => v.toPrimitive()), + customer_postal_code: recipient.postalCode.toPrimitive(), customer_country: maybeToNullable(recipient.country, (v) => v.toPrimitive()), }; } diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-recipient-summary.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-recipient-summary.mapper.ts index effd2116..014a9304 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-recipient-summary.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/summary/sequelize-issued-invoice-recipient-summary.mapper.ts @@ -58,11 +58,7 @@ export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMap const customerTin = extractOrPushError(TINNumber.create(_tin!), "customer_tin", errors); - const customerStreet = extractOrPushError( - maybeFromNullableResult(_street, (value) => Street.create(value)), - "customer_street", - errors - ); + const customerStreet = extractOrPushError(Street.create(_street), "customer_street", errors); const customerStreet2 = extractOrPushError( maybeFromNullableResult(_street2, (value) => Street.create(value)), @@ -70,11 +66,7 @@ export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMap errors ); - const customerCity = extractOrPushError( - maybeFromNullableResult(_city, (value) => City.create(value)), - "customer_city", - errors - ); + const customerCity = extractOrPushError(City.create(_city), "customer_city", errors); const customerProvince = extractOrPushError( maybeFromNullableResult(_province, (value) => Province.create(value)), @@ -83,7 +75,7 @@ export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMap ); const customerPostalCode = extractOrPushError( - maybeFromNullableResult(_postal_code, (value) => PostalCode.create(value)), + PostalCode.create(_postal_code), "customer_postal_code", errors ); diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts index f22102c8..1fa0eb5f 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts @@ -259,9 +259,8 @@ export class IssuedInvoiceRepository column: "description", }, recipient_name: { - type: "association", - association: "current_customer", - column: "name", + type: "root", + column: "recipient_name", }, }, sortableFields: [ @@ -272,6 +271,7 @@ export class IssuedInvoiceRepository "created_at", "serie", ], + fullTextTableAlias: "CustomerInvoiceModel", enableFullText: true, database: this.database, strictMode: true, // fuerza error si ORDER BY no permitido @@ -280,9 +280,9 @@ export class IssuedInvoiceRepository /** * Restricciones propias del repositorio. * - * El dominio `Proforma` no conoce `is_proforma`; este discriminador + * El dominio `IssuedInvoice` no conoce `is_proforma`; este discriminador * pertenece a la infraestructura porque actualmente se comparte tabla - * con facturas emitidas. + * con proformas. * * No se añade `deleted_at: null` porque el modelo usa `paranoid: true`; * Sequelize aplica automáticamente `deleted_at IS NULL` en las consultas. diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts index d181e7fe..f8bb6aa2 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts @@ -455,6 +455,7 @@ export class ProformaRepository "created_at", "serie", ], + fullTextTableAlias: "CustomerInvoiceModel", enableFullText: true, database: this.database, strictMode: true, // fuerza error si ORDER BY no permitido diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx index a7d4af3f..14008e9a 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-line-editor.tsx @@ -89,7 +89,7 @@ export const ProformaLineEditor = ({ { id: "quantity", header: t("form_fields.items.quantity.label", "Cantidad"), - headClassName: "w-[100px] text-right", + headClassName: "w-[75px] text-right", cell: ({ index }) => ( ( Promise>; + getCustomerById: ( + id: UniqueID, + context: ICustomerServicesContext + ) => Promise>; + createCustomer: ( id: UniqueID, props: ICustomerCreateProps, diff --git a/modules/customers/src/api/infrastructure/di/customer-public-services.ts b/modules/customers/src/api/infrastructure/di/customer-public-services.ts index 534a7f6a..9e3e10aa 100644 --- a/modules/customers/src/api/infrastructure/di/customer-public-services.ts +++ b/modules/customers/src/api/infrastructure/di/customer-public-services.ts @@ -42,6 +42,18 @@ export function buildCustomerPublicServices( return Result.ok(customerResult.data); }, + getCustomerById: async (id: UniqueID, context: ICustomerServicesContext) => { + const { companyId, transaction } = context; + + const customerResult = await finder.findCustomerById(companyId, id, transaction); + + if (customerResult.isFailure) { + return Result.fail(customerResult.error); + } + + return Result.ok(customerResult.data); + }, + createCustomer: async ( id: UniqueID, props: ICustomerCreateProps, diff --git a/modules/customers/src/web/update/controllers/use-customer-update-page.controller.ts b/modules/customers/src/web/update/controllers/use-customer-update-page.controller.ts index 0fad2ec1..7c549186 100644 --- a/modules/customers/src/web/update/controllers/use-customer-update-page.controller.ts +++ b/modules/customers/src/web/update/controllers/use-customer-update-page.controller.ts @@ -1,5 +1,5 @@ import { useUrlParamId } from "@erp/core/hooks"; -import { useSearchParams } from "react-router-dom"; +import { createSearchParams, useNavigate, useSearchParams } from "react-router-dom"; import { useCustomerUpdateController } from "./use-customer-update.controller"; @@ -11,12 +11,21 @@ import { useCustomerUpdateController } from "./use-customer-update.controller"; export const useCustomerUpdatePageController = () => { const customerId = useUrlParamId(); + const navigate = useNavigate(); const [searchParams] = useSearchParams(); - - const updateCtrl = useCustomerUpdateController(customerId); - const returnTo = searchParams.get("returnTo") ?? "/customers"; + const updateCtrl = useCustomerUpdateController(customerId, { + onUpdated: (updated) => { + navigate({ + pathname: `/customers/${updated.id}/`, + search: createSearchParams({ + returnTo, + }).toString(), + }); + }, + }); + return { updateCtrl, returnTo, diff --git a/modules/factuges/package.json b/modules/factuges/package.json index 9e85eef1..db0a138b 100644 --- a/modules/factuges/package.json +++ b/modules/factuges/package.json @@ -1,6 +1,6 @@ { "name": "@erp/factuges", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/identity/package.json b/modules/identity/package.json index 08a2db5c..8388a8d6 100644 --- a/modules/identity/package.json +++ b/modules/identity/package.json @@ -1,7 +1,7 @@ { "name": "@erp/identity", "description": "Identity module", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/identity/src/api/index.ts b/modules/identity/src/api/index.ts index 5bccaf84..a09bdb3c 100644 --- a/modules/identity/src/api/index.ts +++ b/modules/identity/src/api/index.ts @@ -22,7 +22,7 @@ export const identityAPIModule: IModuleServer = { * - NO conecta infraestructura */ async setup(params) { - const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params; + const { logger } = params; // 1) Dominio interno const internal = buildIdentityDependencies(params); diff --git a/modules/identity/src/api/infrastructure/express/middlewares/authenticate-user.middleware.ts b/modules/identity/src/api/infrastructure/express/middlewares/authenticate-user.middleware.ts new file mode 100644 index 00000000..7aca2233 --- /dev/null +++ b/modules/identity/src/api/infrastructure/express/middlewares/authenticate-user.middleware.ts @@ -0,0 +1,107 @@ +import { + ExpressController, + type RequestWithAuth, + UnauthorizedApiError, + ValidationApiError, +} from "@erp/core/api"; +import { EmailAddress, UniqueID } from "@repo/rdx-ddd"; +import type { NextFunction, Request, Response } from "express"; + +import type { IAccessTokenVerifier, IAccountRepository } from "../../../application"; + +const COMPANY_HEADER_NAME = "X-Company-Id"; + +function parseBearerToken(authorization?: string): string | undefined { + if (!authorization) { + return undefined; + } + + const [scheme, token] = authorization.split(" "); + if (scheme?.toLowerCase() !== "bearer") { + return undefined; + } + + return token?.trim() || undefined; +} + +function getCompanyIdFromHeader(req: Request): UniqueID | undefined | Error { + const rawCompanyId = req.get(COMPANY_HEADER_NAME); + if (!rawCompanyId) { + return undefined; + } + + const companyIdResult = UniqueID.create(rawCompanyId); + if (companyIdResult.isFailure) { + return new Error(`Header "${COMPANY_HEADER_NAME}" must be a valid UUID.`); + } + + return companyIdResult.data; +} + +export function authenticateUser(params: { + accessTokenVerifier: IAccessTokenVerifier; + accountRepository: IAccountRepository; +}) { + return async (req: Request, res: Response, next: NextFunction) => { + try { + const authorization = req.get("authorization"); + const token = parseBearerToken(authorization); + + if (!token) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + + const verifiedTokenResult = await params.accessTokenVerifier.verify(token); + if (verifiedTokenResult.isFailure) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + + const accountIdResult = UniqueID.create(verifiedTokenResult.data.accountId); + if (accountIdResult.isFailure) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + + const accountResult = await params.accountRepository.findById(accountIdResult.data); + if (accountResult.isFailure || accountResult.data.isNone()) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + + const account = accountResult.data.unwrap(); + const canAuthenticateResult = account.canAuthenticate(); + if (canAuthenticateResult.isFailure) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + + const emailResult = EmailAddress.create(verifiedTokenResult.data.email); + if (emailResult.isFailure) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + + const companyIdFromHeader = getCompanyIdFromHeader(req); + if (companyIdFromHeader instanceof Error) { + return ExpressController.errorResponse( + new ValidationApiError(companyIdFromHeader.message, [ + { + field: COMPANY_HEADER_NAME, + message: companyIdFromHeader.message, + }, + ]), + req, + res + ); + } + + (req as RequestWithAuth).user = { + userId: account.id, + email: emailResult.data, + companyId: companyIdFromHeader, + roles: [], + }; + + return next(); + } catch { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + }; +} + diff --git a/modules/identity/src/api/infrastructure/express/middlewares/identity-authenticate-request.ts b/modules/identity/src/api/infrastructure/express/middlewares/identity-authenticate-request.ts index f03f23bc..8729535f 100644 --- a/modules/identity/src/api/infrastructure/express/middlewares/identity-authenticate-request.ts +++ b/modules/identity/src/api/infrastructure/express/middlewares/identity-authenticate-request.ts @@ -1,69 +1,9 @@ -import { ExpressController, type RequestWithAuth, UnauthorizedApiError } from "@erp/core/api"; -import { EmailAddress, UniqueID } from "@repo/rdx-ddd"; -import type { NextFunction, Request, Response } from "express"; - import type { IAccessTokenVerifier, IAccountRepository } from "../../../application"; - -function parseBearerToken(authorization?: string): string | undefined { - if (!authorization) { - return undefined; - } - - const [scheme, token] = authorization.split(" "); - if (scheme?.toLowerCase() !== "bearer") { - return undefined; - } - - return token?.trim() || undefined; -} +import { authenticateUser } from "./authenticate-user.middleware"; export function identityAuthenticateRequest(params: { accessTokenVerifier: IAccessTokenVerifier; accountRepository: IAccountRepository; }) { - return async (req: Request, res: Response, next: NextFunction) => { - try { - const authorization = req.get("authorization"); - const token = parseBearerToken(authorization); - - if (!token) { - return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); - } - - const verifiedTokenResult = await params.accessTokenVerifier.verify(token); - if (verifiedTokenResult.isFailure) { - return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); - } - - const accountIdResult = UniqueID.create(verifiedTokenResult.data.accountId); - if (accountIdResult.isFailure) { - return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); - } - - const accountResult = await params.accountRepository.findById(accountIdResult.data); - if (accountResult.isFailure || accountResult.data.isNone()) { - return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); - } - - const account = accountResult.data.unwrap(); - const canAuthenticateResult = account.canAuthenticate(); - if (canAuthenticateResult.isFailure) { - return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); - } - - const emailResult = EmailAddress.create(verifiedTokenResult.data.email); - if (emailResult.isFailure) { - return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); - } - - (req as RequestWithAuth).user = { - userId: account.id, - email: emailResult.data, - }; - - return next(); - } catch { - return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); - } - }; + return authenticateUser(params); } diff --git a/modules/identity/src/api/infrastructure/express/middlewares/index.ts b/modules/identity/src/api/infrastructure/express/middlewares/index.ts index 6fa48eea..0e4db0eb 100644 --- a/modules/identity/src/api/infrastructure/express/middlewares/index.ts +++ b/modules/identity/src/api/infrastructure/express/middlewares/index.ts @@ -1 +1,4 @@ +export * from "./authenticate-user.middleware"; +export * from "./require-authenticated.middleware"; +export * from "./require-company-context.middleware"; export * from "./identity-authenticate-request"; diff --git a/modules/identity/src/api/infrastructure/express/middlewares/require-authenticated.middleware.ts b/modules/identity/src/api/infrastructure/express/middlewares/require-authenticated.middleware.ts new file mode 100644 index 00000000..65c44050 --- /dev/null +++ b/modules/identity/src/api/infrastructure/express/middlewares/require-authenticated.middleware.ts @@ -0,0 +1,13 @@ +import { ExpressController, UnauthorizedApiError, type RequestWithAuth } from "@erp/core/api"; +import type { NextFunction, Response } from "express"; + +export function requireAuthenticated() { + return (req: RequestWithAuth, res: Response, next: NextFunction) => { + if (!req.user) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + + next(); + }; +} + diff --git a/modules/identity/src/api/infrastructure/express/middlewares/require-company-context.middleware.ts b/modules/identity/src/api/infrastructure/express/middlewares/require-company-context.middleware.ts new file mode 100644 index 00000000..49b6fd75 --- /dev/null +++ b/modules/identity/src/api/infrastructure/express/middlewares/require-company-context.middleware.ts @@ -0,0 +1,20 @@ +import { ExpressController, ForbiddenApiError, type RequestWithAuth } from "@erp/core/api"; +import type { NextFunction, Response } from "express"; + +export function requireCompanyContext() { + return (req: RequestWithAuth, res: Response, next: NextFunction) => { + /** + * TODO: validar membership real cuando exista persistencia de CompanyMembership. + */ + if (!req.user?.companyId) { + return ExpressController.errorResponse( + new ForbiddenApiError("Company context required"), + req, + res + ); + } + + next(); + }; +} + diff --git a/modules/supplier-invoices/package.json b/modules/supplier-invoices/package.json index 6cc9b7dc..197107e7 100644 --- a/modules/supplier-invoices/package.json +++ b/modules/supplier-invoices/package.json @@ -1,7 +1,7 @@ { "name": "@erp/supplier-invoices", "description": "Supplier invoices", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/supplier-invoices/src/api/infrastucture/persistence/sequelize/repositories/supplier-invoice.sequelize-repository.ts b/modules/supplier-invoices/src/api/infrastucture/persistence/sequelize/repositories/supplier-invoice.sequelize-repository.ts index 5f17d8e5..5a515065 100644 --- a/modules/supplier-invoices/src/api/infrastucture/persistence/sequelize/repositories/supplier-invoice.sequelize-repository.ts +++ b/modules/supplier-invoices/src/api/infrastucture/persistence/sequelize/repositories/supplier-invoice.sequelize-repository.ts @@ -326,6 +326,7 @@ export class SupplierInvoiceRepository }, }, sortableFields: ["invoice_date", "due_date", "id", "created_at"], + fullTextTableAlias: "SupplierInvoiceModel", enableFullText: true, database: this.database, strictMode: true, diff --git a/modules/supplier/package.json b/modules/supplier/package.json index ee35ecec..4fa75e3f 100644 --- a/modules/supplier/package.json +++ b/modules/supplier/package.json @@ -1,7 +1,7 @@ { "name": "@erp/suppliers", "description": "Suppliers", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/package.json b/package.json index 09627dc2..e4e4fccb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "uecko-erp-2025", "private": true, - "version": "0.7.8", + "version": "0.8.2", "workspaces": [ "apps/*", "modules/*", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 15f3c997..7fec53a8 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@repo/i18next", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "scripts": { diff --git a/packages/rdx-criteria/package.json b/packages/rdx-criteria/package.json index 13ae4c51..9334cc49 100644 --- a/packages/rdx-criteria/package.json +++ b/packages/rdx-criteria/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-criteria", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-ddd/package.json b/packages/rdx-ddd/package.json index 1dc774dd..06a488a4 100644 --- a/packages/rdx-ddd/package.json +++ b/packages/rdx-ddd/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-ddd", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-logger/package.json b/packages/rdx-logger/package.json index 93603497..53bed987 100644 --- a/packages/rdx-logger/package.json +++ b/packages/rdx-logger/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-logger", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-ui/package.json b/packages/rdx-ui/package.json index 11dd5f0f..848a1ab4 100644 --- a/packages/rdx-ui/package.json +++ b/packages/rdx-ui/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-ui", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-ui/src/components/form/semantic-fields/line-description-field.tsx b/packages/rdx-ui/src/components/form/semantic-fields/line-description-field.tsx index 5845ce9b..88b5b071 100644 --- a/packages/rdx-ui/src/components/form/semantic-fields/line-description-field.tsx +++ b/packages/rdx-ui/src/components/form/semantic-fields/line-description-field.tsx @@ -77,7 +77,7 @@ export const LineDescriptionField = ({ React.useEffect(() => { resizeTextarea(); - }, [resizeTextarea]); + }, []); return ( diff --git a/packages/rdx-utils/package.json b/packages/rdx-utils/package.json index 2424d9bc..52332080 100644 --- a/packages/rdx-utils/package.json +++ b/packages/rdx-utils/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-utils", - "version": "0.7.8", + "version": "0.8.2", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/shadcn-ui/package.json b/packages/shadcn-ui/package.json index 894b53d4..69c90b0b 100644 --- a/packages/shadcn-ui/package.json +++ b/packages/shadcn-ui/package.json @@ -1,6 +1,6 @@ { "name": "@repo/shadcn-ui", - "version": "0.7.8", + "version": "0.8.2", "type": "module", "private": true, "exports": { diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 5a55a18a..e9a6f94f 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@repo/typescript-config", - "version": "0.7.8", + "version": "0.8.2", "private": true, "publishConfig": { "access": "public" diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator index 1175f514..df11ed0b 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/linux/FastReportCliGenerator differ diff --git a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe index 673d584e..4c242670 100755 Binary files a/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe and b/tools/fastreportcli-net-core-skia/FastReportCliGenerator/publish/windows/FastReportCliGenerator.exe differ