v.0.8.1
This commit is contained in:
parent
7a161717c2
commit
6187a77dd9
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/auth",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/catalogs",
|
||||
"description": "Catalogs module",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/core",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/customer-invoices",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -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<IIssuedInvoiceCreateProps, Error> {
|
||||
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<IssuedInvoiceRecipient, Error> {
|
||||
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<IssuedInvoiceItem[], Error> {
|
||||
|
||||
@ -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()),
|
||||
};
|
||||
}
|
||||
|
||||
@ -14,10 +14,10 @@ import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
export type IssuedInvoiceRecipientProps = {
|
||||
name: Name;
|
||||
tin: TINNumber;
|
||||
street: Maybe<Street>;
|
||||
street: Street;
|
||||
street2: Maybe<Street>;
|
||||
city: Maybe<City>;
|
||||
postalCode: Maybe<PostalCode>;
|
||||
city: City;
|
||||
postalCode: PostalCode;
|
||||
province: Maybe<Province>;
|
||||
country: Maybe<Country>;
|
||||
};
|
||||
@ -56,7 +56,7 @@ export class IssuedInvoiceRecipient extends ValueObject<IssuedInvoiceRecipientPr
|
||||
return this.props.tin;
|
||||
}
|
||||
|
||||
get street(): Maybe<Street> {
|
||||
get street(): Street {
|
||||
return this.props.street;
|
||||
}
|
||||
|
||||
@ -64,11 +64,11 @@ export class IssuedInvoiceRecipient extends ValueObject<IssuedInvoiceRecipientPr
|
||||
return this.props.street2;
|
||||
}
|
||||
|
||||
get city(): Maybe<City> {
|
||||
get city(): City {
|
||||
return this.props.city;
|
||||
}
|
||||
|
||||
get postalCode(): Maybe<PostalCode> {
|
||||
get postalCode(): PostalCode {
|
||||
return this.props.postalCode;
|
||||
}
|
||||
|
||||
@ -92,10 +92,10 @@ export class IssuedInvoiceRecipient extends ValueObject<IssuedInvoiceRecipientPr
|
||||
return {
|
||||
tin: this.tin.toString(),
|
||||
name: this.name.toString(),
|
||||
street: maybeToNullable(this.street, (value) => 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()),
|
||||
};
|
||||
|
||||
@ -314,7 +314,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> 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<ProformaInternalProps> 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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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()),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }) => (
|
||||
<QuantityField
|
||||
inputClassName="border-none"
|
||||
@ -102,7 +102,7 @@ export const ProformaLineEditor = ({
|
||||
{
|
||||
id: "unitAmount",
|
||||
header: t("form_fields.items.unit_amount.label", "Importe unitario"),
|
||||
headClassName: "w-[200px] text-right",
|
||||
headClassName: "w-[100px] text-right",
|
||||
cell: ({ index }) => (
|
||||
<AmountField
|
||||
inputClassName="border-none"
|
||||
|
||||
@ -95,7 +95,7 @@ export const ProformaUpdateHeaderEditor = ({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<TextAreaField
|
||||
className="md:col-span-12 hidden"
|
||||
className="md:col-span-12"
|
||||
disabled={disabled}
|
||||
label={t("form_fields.proformas.notes.label")}
|
||||
maxLength={256}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/customers",
|
||||
"description": "Customers",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -14,6 +14,11 @@ export interface ICustomerPublicServices {
|
||||
context: ICustomerServicesContext
|
||||
) => Promise<Result<Customer, Error>>;
|
||||
|
||||
getCustomerById: (
|
||||
id: UniqueID,
|
||||
context: ICustomerServicesContext
|
||||
) => Promise<Result<Customer, Error>>;
|
||||
|
||||
createCustomer: (
|
||||
id: UniqueID,
|
||||
props: ICustomerCreateProps,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/factuges",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/identity",
|
||||
"description": "Identity module",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -1 +1,4 @@
|
||||
export * from "./authenticate-user.middleware";
|
||||
export * from "./require-authenticated.middleware";
|
||||
export * from "./require-company-context.middleware";
|
||||
export * from "./identity-authenticate-request";
|
||||
|
||||
@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -326,6 +326,7 @@ export class SupplierInvoiceRepository
|
||||
},
|
||||
},
|
||||
sortableFields: ["invoice_date", "due_date", "id", "created_at"],
|
||||
fullTextTableAlias: "SupplierInvoiceModel",
|
||||
enableFullText: true,
|
||||
database: this.database,
|
||||
strictMode: true,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/suppliers",
|
||||
"description": "Suppliers",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "uecko-erp-2025",
|
||||
"private": true,
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"modules/*",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/i18next",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-criteria",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-ddd",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-logger",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-ui",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -77,7 +77,7 @@ export const LineDescriptionField = <TFormValues extends FieldValues>({
|
||||
|
||||
React.useEffect(() => {
|
||||
resizeTextarea();
|
||||
}, [resizeTextarea]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Field className={cn("gap-1", className)} data-invalid={!!fieldError} orientation={orientation}>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/rdx-utils",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/shadcn-ui",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/typescript-config",
|
||||
"version": "0.7.8",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user