From c8c71cf91c4b6de6f5f0f567c85a372d657a5138 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 12 Sep 2025 11:14:27 +0200 Subject: [PATCH] Facturas de cliente --- modules/core/src/api/application/index.ts | 1 + .../src/api/application/presenters/index.ts | 2 + .../presenter-registry.interface.ts | 57 +++++++ .../presenters/presenter-registry.ts | 64 ++++++++ .../presenters/presenter.interface.ts | 32 ++++ .../src/api/application/index.ts | 3 +- .../report-customer-invoice/index.ts | 1 + .../report-customer-invoice.use-case.ts | 50 ++++++ .../reporter/customer-invoice.reporter.ts | 97 +++++++++++ .../report-customer-invoice/reporter/index.ts | 1 + .../reporter/templates/quote/template.hbs | 152 ++++++++++++++++++ .../templates/quote/uecko-footer-logos.jpg | Bin 0 -> 39250 bytes .../reporter/templates/quote/uecko-logo.svg | 1 + .../src/api/infrastructure/dependencies.ts | 59 +++---- .../report-customer-invoice.controller.ts | 25 +++ .../express/customer-invoices.routes.ts | 12 ++ packages/rdx-utils/src/helpers/id-utils.ts | 3 +- 17 files changed, 531 insertions(+), 29 deletions(-) create mode 100644 modules/core/src/api/application/presenters/index.ts create mode 100644 modules/core/src/api/application/presenters/presenter-registry.interface.ts create mode 100644 modules/core/src/api/application/presenters/presenter-registry.ts create mode 100644 modules/core/src/api/application/presenters/presenter.interface.ts create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/index.ts create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.reporter.ts create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/index.ts create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/template.hbs create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-footer-logos.jpg create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-logo.svg create mode 100644 modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts diff --git a/modules/core/src/api/application/index.ts b/modules/core/src/api/application/index.ts index 49bbc161..aee6f87b 100644 --- a/modules/core/src/api/application/index.ts +++ b/modules/core/src/api/application/index.ts @@ -1 +1,2 @@ export * from "./errors"; +export * from "./presenters"; diff --git a/modules/core/src/api/application/presenters/index.ts b/modules/core/src/api/application/presenters/index.ts new file mode 100644 index 00000000..83e6546d --- /dev/null +++ b/modules/core/src/api/application/presenters/index.ts @@ -0,0 +1,2 @@ +export * from "./presenter-registry"; +export * from "./presenter-registry.interface"; diff --git a/modules/core/src/api/application/presenters/presenter-registry.interface.ts b/modules/core/src/api/application/presenters/presenter-registry.interface.ts new file mode 100644 index 00000000..57d27263 --- /dev/null +++ b/modules/core/src/api/application/presenters/presenter-registry.interface.ts @@ -0,0 +1,57 @@ +import { IPresenter } from "./presenter.interface"; + +/** + * 🔑 Claves de proyección comunes para seleccionar presenters + */ +export type PresenterKey = { + resource: string; // "customer-invoice" + projection: string; //"detail" | "summary" | "created" | "status" | "export"; + format: string; //"json" | "pdf" | "csv" | "xml"; + version?: number; // 1 | 2 + locale?: string; // es | en | fr +}; + +/** + * Ejemplo de uso: + * + * const registry = new InMemoryPresenterRegistry(); + * + * // Registro + * registry.register( + * { resource: "customer-invoice", projection: "detail", format: "json", version: 1 }, + * new CustomerInvoiceDetailPresenter() + * ); + * + * registry.register( + * { resource: "customer-invoice", projection: "detail", format: "pdf", version: 1 }, + * new CustomerInvoicePdfPresenter() + * ); + * + * // Resolución + * const presenterOrNone = registry.resolve({ + * resource: "customer-invoice", + * projection: "detail", + * format: "pdf", + * }); + * + * presenterOrNone.map(async (presenter) => { + * const output = await (presenter as IAsyncPresenter).toOutput(invoice); + * console.log("PDF generado:", output); + * }); + * + **/ + +export interface IPresenterRegistry { + /** + * Obtiene un mapper de dominio por clave de proyección. + */ + getPresenter(key: PresenterKey): IPresenter; + + /** + * Registra un mapper de dominio bajo una clave de proyección. + */ + registerPresenter( + key: PresenterKey, + presenter: IPresenter + ): void; +} diff --git a/modules/core/src/api/application/presenters/presenter-registry.ts b/modules/core/src/api/application/presenters/presenter-registry.ts new file mode 100644 index 00000000..155e2f6a --- /dev/null +++ b/modules/core/src/api/application/presenters/presenter-registry.ts @@ -0,0 +1,64 @@ +import { ApplicationError } from "../errors"; +import { IPresenterRegistry, PresenterKey } from "./presenter-registry.interface"; +import { IPresenter } from "./presenter.interface"; + +export class InMemoryPresenterRegistry implements IPresenterRegistry { + private registry: Map> = new Map(); + + getPresenter(key: PresenterKey): IPresenter { + const exactKey = this._buildKey(key); + + // 1) Intentar clave exacta + if (this.registry.has(exactKey)) { + return this.registry.get(exactKey)!; + } + + // 2) Fallback por versión: si no se indicó, buscar la última registrada + if (key.version === undefined) { + const candidates = [...this.registry.keys()].filter((k) => + k.startsWith(this._buildKey({ ...key, version: undefined, locale: undefined })) + ); + + if (candidates.length > 0) { + const latest = candidates.sort().pop()!; // simplificación: versión más alta lexicográficamente + return this.registry.get(latest)!; + } + } + + // 3) Fallback por locale: intentar sin locale si no se encuentra exacto + if (key.locale) { + const withoutLocale = this._buildKey({ ...key, locale: undefined }); + if (this.registry.has(withoutLocale)) { + return this.registry.get(withoutLocale)!; + } + } + + if (!this.registry.has(exactKey)) { + throw new ApplicationError(`Error. Presenter ${key} not registred!`); + } + + throw new ApplicationError(`Error. Presenter ${key} not registred!`); + } + + registerPresenter( + key: PresenterKey, + presenter: IPresenter + ): void { + const exactKey = this._buildKey(key); + this.registry.set(exactKey, presenter); + } + + /** + * 🔹 Construye la clave única para el registro. + */ + private _buildKey(key: PresenterKey): string { + const { resource, projection, format, version, locale } = key; + return [ + resource.toLowerCase(), + projection.toLowerCase(), + format.toLowerCase(), + version ?? "latest", + locale ?? "default", + ].join("::"); + } +} diff --git a/modules/core/src/api/application/presenters/presenter.interface.ts b/modules/core/src/api/application/presenters/presenter.interface.ts new file mode 100644 index 00000000..3703cbdd --- /dev/null +++ b/modules/core/src/api/application/presenters/presenter.interface.ts @@ -0,0 +1,32 @@ +export type DTO = T; +export type BinaryOutput = Buffer; // Puedes ampliar a Readable si usas streams + +interface ISyncPresenter { + toOutput(source: TSource): TOutput; +} + +interface IAsyncPresenter { + toOutput(source: TSource): Promise; +} + +/** + * Proyección SINCRÓNICA de colecciones. + * Útil para listados paginados, exportaciones ligeras, etc. + */ +/*export interface ISyncBulkPresenter { + toOutput(source: TSource): TOutput; +}*/ + +/** + * Proyección ASÍNCRONA de colecciones. + * Útil para generar varios PDFs/CSVs. + */ +/*export interface IAsyncBulkPresenter { + toOutput(source: TSource): Promise; +}*/ + +export type IPresenter = + | ISyncPresenter + | IAsyncPresenter; +//| ISyncBulkPresenter +//| IAsyncBulkPresenter; diff --git a/modules/customer-invoices/src/api/application/index.ts b/modules/customer-invoices/src/api/application/index.ts index 14646c6b..97ba1ed7 100644 --- a/modules/customer-invoices/src/api/application/index.ts +++ b/modules/customer-invoices/src/api/application/index.ts @@ -2,4 +2,5 @@ export * from "./create-customer-invoice"; export * from "./delete-customer-invoice"; export * from "./get-customer-invoice"; export * from "./list-customer-invoices"; -export * from "./update-customer-invoice"; +export * from "./report-customer-invoice"; +//export * from "./update-customer-invoice"; diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/index.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/index.ts new file mode 100644 index 00000000..ebba15ce --- /dev/null +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/index.ts @@ -0,0 +1 @@ +export * from "./report-customer-invoice.use-case"; diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts new file mode 100644 index 00000000..9c1b0437 --- /dev/null +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts @@ -0,0 +1,50 @@ +import { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { CustomerInvoiceService } from "../../domain"; +import { ReportCustomerInvoiceAssembler } from "./assembler"; + +type ReportCustomerInvoiceUseCaseInput = { + companyId: UniqueID; + invoice_id: string; +}; + +export class ReportCustomerInvoiceUseCase { + constructor( + private readonly service: CustomerInvoiceService, + private readonly transactionManager: ITransactionManager, + private readonly assembler: ReportCustomerInvoiceAssembler + ) {} + + public execute(params: ReportCustomerInvoiceUseCaseInput) { + const { invoice_id, companyId } = params; + + const idOrError = UniqueID.create(invoice_id); + + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const invoiceId = idOrError.data; + + return this.transactionManager.complete(async (transaction) => { + try { + const invoiceOrError = await this.service.getInvoiceByIdInCompany( + companyId, + invoiceId, + transaction + ); + if (invoiceOrError.isFailure) { + return Result.fail(invoiceOrError.error); + } + + const invoiceDto = this.registry.getPresenter("").toDTO(invoideOIrError.data); + + const pdfData = this.assembler.toPDF(invoiceDto); + return Result.ok(pdfData); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.reporter.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.reporter.ts new file mode 100644 index 00000000..f052e72c --- /dev/null +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.reporter.ts @@ -0,0 +1,97 @@ +import { toEmptyString } from "@repo/rdx-ddd"; +import * as handlebars from "handlebars"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import puppeteer from "puppeteer"; +import report from "puppeteer-report"; +import { CustomerInvoice } from "../../../domain"; + +export interface ICustomerInvoiceReporter { + toHTML: (invoice: CustomerInvoice) => Promise; + toPDF: (invoice: CustomerInvoice) => Promise; +} + +// https://plnkr.co/edit/lWk6Yd?preview + +export const CustomerInvoiceReporter: ICustomerInvoiceReporter = { + toHTML: async (invoice: CustomerInvoice): Promise => { + const quote_dto = await map(quote, context); + + // Obtener y compilar la plantilla HTML + const templateHtml = readFileSync( + path.join(__dirname, "./templates/quote/template.hbs") + ).toString(); + const template = handlebars.compile(templateHtml, {}); + return template(quote_dto); + }, + + toPDF: async (quote: CustomerInvoice, context: ISalesContext): Promise => { + const html = await CustomerInvoiceReporter.toHTML(quote, context); + + // Generar el PDF con Puppeteer + const browser = await puppeteer.launch({ + args: [ + "--disable-extensions", + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + ], + }); + + const page = await browser.newPage(); + const navigationPromise = page.waitForNavigation(); + await page.setContent(html, { waitUntil: "networkidle2" }); + + await navigationPromise; + const reportPDF = await report.pdfPage(page, { + format: "A4", + margin: { + bottom: "10mm", + left: "10mm", + right: "10mm", + top: "10mm", + }, + }); + + await browser.close(); + return Buffer.from(reportPDF); + }, +}; + +const map = async (invoice: CustomerInvoice) => { + return { + id: invoice.id.toString(), + company_id: invoice.companyId.toString(), + + invoice_number: invoice.invoiceNumber.toString(), + status: invoice.status.toPrimitive(), + series: toEmptyString(invoice.series, (value) => value.toString()), + + invoice_date: invoice.invoiceDate.toDateString(), + operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), + + notes: toEmptyString(invoice.notes, (value) => value.toString()), + + language_code: invoice.languageCode.toString(), + currency_code: invoice.currencyCode.toString(), + }; +}; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const quoteItemPresenter = ( + items: ICollection, + context: ISalesContext +): any[] => + items.totalCount > 0 + ? items.items.map((item: CustomerInvoiceItem) => ({ + id_article: item.idArticle.toString(), + description: item.description.toString(), + quantity: item.quantity.toFormat(), + unit_price: item.unitPrice.toFormat(), + subtotal_price: item.subtotalPrice.toFormat(), + discount: item.discount.toFormat(), + total_price: item.totalPrice.toFormat(), + })) + : []; +2; diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/index.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/index.ts new file mode 100644 index 00000000..8f6ec562 --- /dev/null +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/index.ts @@ -0,0 +1 @@ +export * from "./customer-invoice.reporter"; diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/template.hbs b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/template.hbs new file mode 100644 index 00000000..a14b7fe9 --- /dev/null +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/template.hbs @@ -0,0 +1,152 @@ + + + + + + Presupuesto #{{id}} + + + + + + +
+
+ + + + + + + + + + + + + {{#each items}} + + + + + + + + + {{/each}} + +
Cant.DescripciónPrec. UnitarioSubtotalDto (%)Importe total
{{quantity}}{{description}}{{unit_price}}{{subtotal_price}}{{discount}}{{total_price}}
+
+ +
+ +
+
+

Forma de pago: {{payment_method}}

+
+
+

Notas: {{notes}}

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Importe neto{{subtotal_price}}
% Descuento{{discount.amount}}{{discount_price}}
Base imponible{{before_tax_price}}
% IVA{{tax}}{{tax_price}}
Importe total{{total_price}}
+
+
+ +
+
+ +
+ + + + \ No newline at end of file diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-footer-logos.jpg b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-footer-logos.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dbaaf5eac2aa3b13a96cd9332b9130fc1a127fbf GIT binary patch literal 39250 zcmd?QWmFx}wxC8r&hchcvn8-gD0FH@f?c z{_}pkwfCs~)m*b`&6-PU+5WTqXA3};m6VYLz`(!&>Gu!tX9rPJMoi2=QTdakjGV-K z1OOmP%URhwfTIF{oxQ8GvJ{9^Q%jo^W(oiY-~ecV6#$G(TpZ<9Bvpa;AR{40>hiAi zm;V*+7lHRX0l*xiv?3|#zvTa~2+hRN*%bi56yLSkP0d_P-Z}W4Z9QBa|H?1l8PnM2 zFM~t=W#@MT-x>EWTl|9={zK;?C z%;TNs->soe`Z?mBrp!003YSE&h#-|BYSE zJm2jE05JzgFJ~(YOIK1F6IxPMUS1wjX)_O7GgnteMI#d%BWF`mF$X(GBYQ6Z_{TH< zy$V48>s!)yCv&m#a&a-TF}=J0pW%NS`A@C?J^anve`wq&{YRdGVNd)^_OEOIC37eM z0G_*d-^Be(W}FTH%^?7QyZSE~MIHd41Oq_R^uIk1_TTbi>FVmp$IR^E;lX5OX2SH> zq5lm3M}>cC{;%QR(qsCY-amauDsE<%&ZI7mMkZ#YjQ?{X{=dHP z-^}_qI~bJB%*~w5?BA18e=lWL_7?BiZEtGjYUN;0YGwbw%HjXZ%l^%Vzxa=G{SFx4 z-T+JkCIEFB6M#6M03b0C00`Zj_ZYB$=9?UxI`FsVX^t}vl6=`fWr-7r63_F*1j5nxGR*$;fUgx;e_K<!5_pwBfuo!Comz1AgCu;B?Kp=BUC2zAuJ@EAiN_YCXym@BuXdhCpsg> zCKe&KCQc&mB0eU;AQ2+5B1t0YCOIL+A{8aIB~2q8AiX3bB$Fm{Bg-e7B76Bj{Xy+R z;D?$I8|3ihJmlu&N#uRxmlPxv@)VybDkxSd;V5}1Eh$qehbbSZsHrrlzEU+)9Z};_ z%TRk$S5U9hAkhfZIMEc)%+o^C^3dAQX4B5lfzxr)S<+?FP1A$ZbJ1JTXVcFzKr!$# z*fSI`EHNT7iZZ%0RxoZeVKd1w1u``=T`*HHYct0)4>P~9aI)C4d}CQ-MQ4>|4PzJE@+mJhp zdx-~=N0BFrXM`7u7sTt&+rj(9$HnK$SI_s0pON2&znuR_fLg#z;G4j%;0Hk?!F<6j zAyOejp**22VKQMO;R4|u5poezkz$cUQCd-J(Mr(^5DUl|)ChVI;}i20>k)?#mlBT< zpOQeAP?N}z*p&PrX(3r5c`3yy|*izUs#q!Kb%qqj`%39hw*ZRTclTC@u zA6pIE8ar4!L%UXcbbA~70S96SPls7YddE=5Ehj#wWT#8#kIuy|U@m$tEv}gFov8^o z8n+O)ZFeE}O!r3*b&v0!XrA_-6JB&);a-Q{lHNr=kUl0peV@rb2YlZ474^;c1M@TX z>+>h~5BA^xBK4&-06xGbU^0*;Fd^_ZNF%5%m?-#5@NS4yNJS`esB`GjSN^ZLVUS@~ zVN>Dk;pyRT5vCDikt~rZk*`rEQDf1p(P`0tV$5TvV!2{-;-KT~;}+wE<4Y6J61)?3 z6Xg?|l1P&xl5Ud?l1EcGQgTz_Q{7Ux(&W-w(kas8(qA(yGZr#InKfC2S>ag^*{0b) zazt{fbBS^zbD#69@|N?Z@|y~%3Q`K83*8G3zNvp3F5)RFE5Cq=T#@vlFW`whO*1xa&{1clTY7W6xQy zMelB(LEl=xX8-(v(!kW<$H9>y$)WyX(c$h9!I6$pzR}h(p0Va}uJNV`&WXlJ&dJ6p zuBoPJ?&+2p-kG*pf!VGf!asWF#OH?QW#-2h6c%O|)fShRbeFc4O_z^WY*(&U-B(}M zzN|y7M{J;Oq-+vy7W}0BS+m8p)wwOcJ+Y&*v$kupd%EYc_qrc^fOL>@NOD+q#CFti zEO|V0qI0r;>UjEc7J80$o^?TU(eO*;*Tkjf<^GlP)t~FA8@!v6TaMelJH@-rd%OFW zhloeK$I>UBr{QOf=ff9|SD4qd-?YEm-afvq|FQe?=g%4-_II4|e+=N@U|{cGF!28h z|03|eH~F_4{5Q4#QvW~x{^1jC0mOGja5M-sKp1$q63J7f!Gwnb5BUE9)bVyS z)IRztR0Kv2X2;bxyZpQumU!3}k0Png>)^h4*&Ku2FL5I02;XO4R^?2uo1BMFDA6uH zQRHBfTWr+?vV}iHhI=JOMv3irPVXuD*5pcPeJ9P%>07kL&)mF5h%mEUgsNMZK(tiL z=1aP#`z(Fodh8j&WSF_T`bFzZi|^~ekTVlq;jk>X$kzM?XiIHaAp|z=lH0eLRihfV zJ;B9mX!|!7%mv0sj%-HV_rdGYV{+pp-mU1N<3OB+P|)zs*JIC>Bxe1zQ6)GJIJsI% z@!TIudo${7vT5mr%A9T0AIu^zQiEC{!j-m{TB&Ws+)n3;>D*7t9C?(3%4xi!_ex&q z;F~;}9wI9)cW4DOQ)_ILjk&ADa|clA6+?PNl#5tGw+FVuMWV#ldk^vw{$|8_NZ2;Gd zo*Xnp46pKolp;tf>z2K}bf?OtKwM%>!rMR(fa3NLJU_g=e459CSaDQNqz`}8qHc+! zotzq37a$K`u*AS&!i{DZ+=-&M6@~+G$#gCqCh<62P5h+C@jR8i-Wr3U?+qjo zxN*2oL=oJ)BPB{_<`IOb`YdaVP|Mch;&#GUxN}zMGIw3Z5uNloqUwhjS^ihE-U^j_SnO#LTa&)bexf_!KoQz)X<`!AP zxwqz_-}ixH|EICPw6_g+tGz0MM~+U zU~3iHPiaxvvL+~L)L{Mlr3xaSmzpnD16NKN+_$uAa8f3|1Wc-Y=r^vVIL?XpxM@rx z{R13>{Uiyws&Y8Cqys%p6@fU)Hc-<+d*IHZAxU5PD@FWXMe4XW6|=glnVu2u{?~0T za%oz-k{!D(Yo5$^a<)Sg_aLuDk6Gg{P9rSA)X6nA9Hr<@Zn|Y>@p-(d$;t{OPKUyr zP(>)#ll3wm1I?=ni4w*G+BZm}J9EEQ%1M0<%fQoGz1V?kcdovDz>tWtSN=M7)VUMr z8*{q$gJ`3+0FEnhUawlP@hZ5Ulk?t7sNZ_BauciHL#y2#D8UiYpmLUVOJ5%T0Cr`~u@ zQb(lZZtjuDL#`X)Q{`dW=b4VcCw@YNB_eQN9XLAoz`btOpWQNe)qcbf!A)&NwZReF z=sg;Yq{`5m=ciQ!feSm7LU{BHod*zrPx&hCL&&v2-H7$N_@d0rxPzh9w zsNu6%sDuI-$Rm?<7vGz6_~-w4vDNZ)*L^o75D?^SI4DUiZcmKhz)=bFE#7KN#y;FR zX!fnT@t9HazJY&R+xY`%>Ig?G3VMb3+g35kw~_4|r^wv_*6-%Qx7sFu z{Jj5`f;Hx;r!C``6vqmga!3Q!)8J|G^j{$M!XzF6`sm;j61+PKwchv9i9G@J4tv4% zqr>3VBN=NDy7XOj<7ZPtUy4Wz_XwNB24TF;F*~8#O23mu2~j zYCQppt$}ql4<~S4J!hFE#y@cBXoRv48p^y&ud?FD-1A0>Tu<>eLZw@M#Fqsj_%!Qj z>*U8DjHi*rridMQ80(+~>vJTyg@Tvo8snFBDl(nbHx}=k{Hu$-SSs7;4i=jS&%$xU zDyf6f(IBhrnX$VAqQ+l25FjQ9I4hd>l)abtV7iFh_@XKabT?sWJnVZX117dx`%F)G zhqyzbtghRV6l6Xpf4Qm48yEUFFO#&$=cn(74Kqe13S4*L8U@`Lv5(|d*|=$R{!T(~ zg8~myeQ@v1Do+z@P$+Pen-%4Q7L{R`Hc##?-LKz&^9OJ=HEV) zb}Ey|Y9=KU0(x^1vGFh`JtAiKC0Vo$!nEMyxu4C9OYpWTAtJc3A^{ktB=+YtOvy(| zIDhBBw*TyaAvf@YI_z+W?pVTlQhuo^Sl&)J?cmN`fp4D%MwJsb%|zs@+J_h?zlC zxCkwHXP#_OHARQ;F?3#joP4_q(Qd=4Cw<#6jdN8nT29%eRyH5s z4gNxi@?h9t=D@vRscXM9CcF#1j$tDD8-(5YfMXOxd4t-(Xz%2h@J?s9_)^dV7ata3 zf&L|x51!JP;?o97hZhG%DMuzv*Yd2q_e}$2g(t)8H+%VHgj%_ep zfb;1a_aDIR*blC2=bn&*C~D>(-C0qnw0^CF$TM$m)iPt3Ci*tcuq7HxH&gf? z8kn?fPmz z_Zp8Wi4^2VysN0xY4w=7jYi{45MzxxoG1Y@Z7p!64NGl}NsMsFIM~4Kzf<*@pZW?dpubEplrx5oX zIA4!+Y+m}L)FL*d47a|}>m=fl&hSB%tuu##+DkvW5rR7-Q(nj!-sp@9v-wj6rEsF# zM-ZKzx1!(s`?$q1_LLEb+thft=pHq~^5Qd=d0sPAO^5wAiy0;I9uwf53?9ztay1y}s zOMbhRmsgK3DPOV^2+rHkpv&32i14HB;?5QwNz~-XQT4z}jZ96xQTCbLV?5myX2R^U;k~hSA`R@R~ZKM3(a5O}o!^L-P!jfG*Ru{ZY`DeQ>`d1#H2ptOF=0D6rBfFeYjr9FlP9>V0hS z5f!TjjRUrlq?P%};92rN*=LMKWM7?Tj{SQ0bpaDht@`g{(m<}F5WcTYf^o%zBHGEE znmkv!UX;3qxce`eiKP$T2-?yarp@AJ2v~H3O?=XuzEZHyDSrU73o{}%BVBkNJO7Z4 zv%D?K@GrbYj#DDnPoHt}JwzlT;*)V7HC0)I3cw-zP%+{=9cWwLSUXPO)sF!DIC9DG zm1SAQRw`%d-Rw2lJSs7KL{;z>0rp}(ANHWgmcWA-CVP8pI2{lozP4iHmwhgzL}=?eXJ9UM}`4s1ef9(fwAWR(rxdaaH+-`3h1d(>qXLB;tH2T|||VX33|$ zKDcuqhcRgQD03{YR0_q`R(EJCml8*FY$`z@>tK5#y%CVQj$O}qyHtaLlT*?Zd=PsG zn!tcOcz% z0`{APa&S!rYNrJ(hKi|-*YPDVrfdWKK6iD!%1m@Mu=%4E3*sr{Qjxga?B6}?D45;l zCq72A9*wy^vApEwGS_sYyD(QF<_=iJN~)BMeqDck^7Va8O!u`keIu9iX3G|m6FNK` zKm9e3HGV2aeT8o_n{<3_^dKx`VBkk_V4#gB95zQBRq`njrS<9h`Q?h5?RjSORWj|? zjPG-r@AP!)($8?-)Wy(GI~>D>v~zp?7yq9a*Y-sGx||K#x(>jM)levmhEIZp-|X3% zcrsr{$l=j5%9Jp~0vW9dOQ#@fWzGz}8Zr4UoBpT4XP=&mI7oKwk^TTb%lzLKiZ^)6 ztj4s0Dn2ggk*Md4v!ZZIO%NA;T{~FE?xu$E)k2BFDrCh9ww{X%_Zw8erGbM@U2*a0 z@$JC5|GDGW2s68sFDHPQudRrf3tOy62AkVr^TJ;v`joKed5~nPjp(f7x97R1q{G?V z;#D)Z`Ui0JBK}F=eV8{K#{ z>&EDltWZIPQHEHt8&`Lk1UDM?7hG%5yg9Q4D+on)zs2Y6H`Gvo zXt@s+IcB;W;i*18eB}0$v=Rv=rDQg zy=1tcKHp0fr?ON>a%1u3z^YT-tQ31awZw?3N5g>pdf?>b$1pR!dvKP*Ze_z6rEfN8 zeyxz{qfG7d5@P7u0R)R)$G2isbU%qwhqWu`v&Cfx;;8k=K>6?=n!^oK7!(w#54e|S zw0pkSfftENr%)G0Nx*0PfK0Q&CMclH#nyt*?nw16ncK*k+CH-FPA#lo_h#~IAI%$$ z-%whqHyl|ak3JC;IU2e|OycH+(UxGrZ&+j%n?Nl!MPob+eb3Nt=Rl>`>1UEF>e4?z zkf+0*HO!6hk^ewW;TzI+cYGdBA$rCPfl&4`{Cby6?Vu{gczd=$won5h<->qBA8wv2>S_h zNqWEueR%wP*f*Xn*WDrhQ$LGEDy?fDN{ebEHp@1(>d5Np%xhu3!;T7z#pO$>emH$W z_h-#8X^oo?YJI4yqXOjWWL&i?Y5guK{rNGB-qp4b-}8_P9pXV2$a)Bb(PbF@A4G3w zS9q5Ppk#CcV|zC+3q(vl)rmy=Mv4k4q9*{l35#YadN(LeE0(-1ZnW|#LQ)fbbBOJx=^vnY`cBqRQK2;MvXRQ zhIk)xbsZ{niS0yudU2*h8tV4<_&)pV;)Uc!(aOLyfma-*r6DUBo)FFm0?95%RR}s{aIX_z_x^JetcF~y1W zNd23U-`IM9QJ}f&r`d+yG5NiWs!AiVqxdthU8Ht(@#M z8kC_~BceODerOcx>}6<6jW?94rjde=`tp!f#OF&f*F36f;lHR;eFMkJ#rw=_^*|gG z+v1JZX4NIa9>H2_85@vM5;&|;KT1Zg<=3%E?Kbo>d{b*P2we2HYY4;Pk}5=IR)XWF z&K2HwVg*iyman_>C^K2?!#WreI&$i;k@!?3uDMj&X0cK z;CilR>C@V8PaZ6KxyvaU8v{B@J>{O4%Yb{K@D>nJWOaCQ8*8_qWkYM@#}u~jl3cbG z8WIdcuiv&ob6BVfh3PA;or>U#>9y6Cv){N|0(d)mW=vbzU+sx+H8b5C-siTa6lIE0 zChMR*J%>)>!8FrJbiawj2{yxRdpWzC$v;;PTClI z_k?c9m6x^+zEAXamalBeTqMq1+~C!J$$^{q(rdingZA{5XDkdkw;_)ot<{eiVg-zW z@W&?t^`V7*GDl0bElJsB<#JeAQ~K7)2@G;rxf~Ay6_?5Y1YHF;mXGLMKx)^(3VK_E z2$P1{~uA zOOVu{N}P*dTWl_x1$mu{I#|^BUy^y*gun&Q7;Y4bKoUZsqAd=6t$gule6hJLLClQZ z56~U=YHq#>R*PDM;pZE7$3tSZ2^IR6dsRJ8MJCJFrKgIb;qz%$bK%)#upKqgM=rV3 zzw%yVnccgsj?Y|cYQ{{?wY$(LKqF~p={SA!no$1)tfm)^~$tJKd|JZ`6WrvZxI z1nQWpCLihLA|D3*$K1RSCt8z&%x8F zJDx8m_W)NT5nL{6iEQf7hds~Jgb?L#*~SB2Zh1G4*orbn1YnZe(ci`A0-niPVtJsx z+vdqVU#97txat@sKYO4uucB8#hgX2xD9_`I>yqb<1Zc3%xHn`Y=0jhD&j#nPY{)9H%y z*&_1cH%`L$m#H9AF7e7Z8psn@4siD=9Mt&n&-9$B4p;P2c#G|>8hUdXBTPDEBf1@u z7q?y3$luBQGd#7B-c0Sv8JDzpZIU48tcS16>Fzx0J5BBd9YH6-;^(}`{H({0wP0G-^i5)AC+$iE)ViD?BqwA8{f{-@u};n z&L}`9sk3qs()-)Lx9mWE7>zW_rc-dgyJ7QLEcpe_Ox7&h^EL09CI{geV>nEeM=oN4 zB8T*1ab&ZCHpn*}m&-2@bS;B>X#&lFSij?pyV5_WGeipoYxwm!@13n>h+>N6NmrF2 zO*L0nzq7?x3oM%u0)D;#W>~qGDzGD-WQH{9YIf6y=GD94%1UiLwY)M-gzv7Cxt1j$ zH$Fe8F0(MWmV&f?kq28?CI8CEcmk7X@Z;9MThY$Kq?5lJKGmEe?u@asti*B2oY3Sb z=SKYsMVE4v_&Ktze~+8a3W61r3?VyWZ9L)h~71MO9Y4b_~hnQERTO zDb4jMCKo+Eybdx@<~31PIpct9(M{&3ixV0y;{TXpWxT*M$k$8ZzI7hN&)iO}e_Mn6 zxz{`?x_7v*Vdckm53~40vX2ilR{i7E>d1%Y5DGT>tS2HBze7+Ln9$_&c5r*wG$s? zO&_?R;PM9%w}6Y^ipjZyIDdc%8V0>EF0*?N3rpD|I0rCun0Ed~2u zZ3ra%c=9jqs(ADbkW|;dEm%ItP;}9}jpO>e#R`iTeVYB=%tvO=ws)Y5`x55pDe49b zB8O9q-g~%o%$>?Rv8d(vWpws?wT)e9MTbwWj|bv31*zg*Zmncckua?%tM_;_SceAP zt(=B?w*eEu$tJBLXMBdA$(5R>+_*#V^w{OcWZW>Nv8YYXN#fK^-I6`Vm9 z38gSxDIGtVdyCyU@qBCi9k}M(SZpEwaOgL!lt~A9FI-^sE{a;wxI~tdT7kTXHrjE+ zv18Xo(cyjKKk6rJcjw_B!2Fh&F3%jo-JeMd+=yo~iAmgyU$H zJzk0CH%m3T9(6^wNW}STZ^ASL1|F;DZut$A*rcWDGvf9)gvOuO=9&7Mx7zb&RWiD2 z2VQLKDTsBQ4z86WuqTga-MXh#i{_HOhhZ0u3zN1!U{u58=B&Yf$y473dijjfp0@ZW z^gUHzv(qByhM&r=k16~>!7 z;*S^%kjh)iUl9*qKdP)P7G1#$wr#ChU_PJF9}%$Z1kk$P654Fo>Mn%t*~%nIuF&_R z7jWTpws#6#-9jB7#(1te=qo)v$d`_5$8q{V%Y}$4Ge-BKl_Tq#%T}n`+@3(By47FM ze8g!I8=vjszUZv-J1hVDpp25*^Tki{pWk{Upd&^uxV;^FwSGx%2h#6v6#*B-f{!+p$}DG}z{1F$4hZ<4u0{#i=QsUsPqh z#KhRvD-;Q!eQp*#QL7-`Cjs%k4$m=xdD-BaLdn?hiqY+(BU}F$piSFB}skYX2;Gsheju$hL z^<`b*aY-BdiVaO5%E(}9fa;v!Y1dmobxzveVaNZWek}sjSgCJpK(X$MTdzXoaCN_= z`6|FV$s-@PSQKAwRAQ#=nPKteq%#IiH4ZiV_rh6?$j13az@C09ikRq%d^u&@_}lH%2tKYru=qOq%%ndHh_# zmd1o(6Qx47YHvXA6haNT(>wz^RdI8OU`Fu|Slyyw94w+Xa zplg>Kqa1~FN)<=q3~k*=1@|_{K&bsb(a|N9)*`VmNH|BN&DbJjIB0w~larrXZWd1# zqb`Z^3YFYWGBTe>>?oZG`jw)-Qn{$t-h^!(B!Gp>;|)G35n}HGDfs)9TX-|CWyWSb z^_jX+_%VCE`&2(CZ+F=nnqSCQ@vTdg#!9ubkD{f8eI=49U6qF^-%@k2s5fp5FN;5C z-sgu2WMB6=HzyQ=zlHR;4dH&0GR&Pw8dOL(1|MleyC*q^wiEQn1&V%a#EDe`77OEIbWW!*lDxGFXmL$AM&Gw)X?;?@ig-?kb*~3ld$~wKG+}{;0 z=x8!zn*@uNjIGGLJ&2Y7(s@1>1-~hOTE9|bX+VIR0)dn z9r5K2>hBk=T&r}E>hxc6YBkYK9!5W{ZycImKSx2bmt7i}$VNbPoPZnn$c!PF_B6vp zn?lZZ5g8Orc7U|E&N4(KJzI6(-XLG2$XT%%ZVb7Ango!)-dK-p-#{0BNG9S-3eQ|w zofD!t&@ZjPsyuDMgel!dg3(8A`)T3skYF?V8s5QpN^npyu@)$?ZQwe+dLRw(HP!I> zss7$~1lbdR$V}rPT9X+@T=}783pQ>cmt)5E+gBiprQe`H)F#Ssn%3SXuCY~$gkVPo zgNIgTTb?ekOeLcK?PgrGc2z;Mz@xOWve#V8bmupMk@C&)+S#SwcSqov{14ED(dX9p z2cU%f)X01jo&E<16%$?>seV?E^=C2@+z?2j-9u+4FYQnVCT?G-ETGc&+tm;fSlU{$ zqxY2?>M>6;7fYE{OGULVgvD-qTeV{qxKAog$HzbTrB6~bh3iv&FB3AnN{9a@{R};j zqOh3-ay=Zyj4#>b(lsH8`$mm^8&)sw?o}8%(2QwQFK<)PQp2(WdCQr&E?}7A#gs>KJ>wTw zlD_JXp~rqYkQ><*^h-k>KPDw=l*7FwB_Y|t0j&A?YNAWUMnw*%c)OjaPjR64vQp`_ zlv2J5TKpZ~X9DAOq=>8vQxg7VXdy?YU`*KE7J{Fqe<3 z+-s~QSv-^UlW){Cz-ObS9QK&mR~Rhuz)}hNntM3}hnOO>IwMz`Rb25RQa9Qq|ATg6 z)NPJ8EJvREu6G`gYDp}Sv(N9T^M|0DFYor}^D9iFLTWs#pj@Np+B`N}4$kQ={16-q z=I@7@+(wP#36Zf!pQqHs9lU{}AmoCHusYsXhYyDP=AVFi2L0aEU%)rW7mhK0?8=)YGWw|$T|Jkz7 zMf!Ic`rvjw32u%IVQRU8p<%TOOK`j7tn>xssQf$)M#A1n6f=$FjbGMKUO>ma-elvq z3GX-Q4%_M4cu`7Nsv3LyAl2m`d(jsRWwkxBI$uV089(5Cb)ec#V-y~GdapOB7IjT+ z8zBkj(r(6O-cJdkS%?kJ9LVT`MnX$$NtUU%#ixlWzv>!~BFYTdgnePwMI})~X83+t z^Ro;!4wSi))`qPfHQDS%!^C+nG1vqPaPrlYb6AO4CQ7K5@#7P>DrW2Yxe3CGjS!=z zO_1NTpW&`=V*W(;yj-?zqUwY!m;OcAGVy$c>?+rYwz8wJ;rlhoP9*As7FofYK!fzP(B^hrKet*DP4M zM3D}eb;_GY^v19z*>rNBGm=7QXF~Zn77XIm$<~$ftrz3eY2C6P>jwCSdw{ph8;jy3 zzo3AO+gW~N(RmklyM~o}ZJ+cpqhyzM{uTcK?SZ5Zjo$#HJW{AUk9~VcOe=0gRaWnb zH+fp`1t+vx(4bPUF`B1~cL;ofMB!Q8-q?cn`YHJQIwVh}@Ptrar;r3U$z(-lJI5&y4;BTfg^!KRDKxx!#>sShzK*mo-XXQx+tytGanUk3Q>>-3<*ZJ^K#jm$Lf7NQ-!$lCATx_)YYZ?bvoB= zDSFg9y^?Eul-HBo9aUYZNl*h73lTJixw#W?@6AfFY@0yv8x(cy%*096&apwe+&#R zF#XCZ5tBTDBTY}OsD>oToTQ>1Wc0p0359{E-_~6 z1OuiVY>iPVnK!CN$&ys4o))T*&z47!1+C!vX6**9B)XXrbB^5X@BVQ^u6|InblFkQ zjLpJVCHwKazl{OPXN7T!TVLWATE;K=Y@0%V06`S)7+FOx}Xq|8*S`ukc2M(q1`c?k3nKJVn}=X4!aRd-_1`3*wR9r#DasUt3C$ z(yr27LMw$k-Ku!LV}js!mOQpQE`?Yq`)Uyo1mT;9LT;&Wjy6)nxf#l(ZsfslH6mi|6~R90oSIRg{WVt5B2;BS{^! z_!>Ji_2`0CW{BeW^JTq*P%7`XeHEd>l>B>x02{4>tZr5|Z=!^8DIyx*NNO+{>-kh6 zbK?=jJay{=&ep?k)-lvLKBqK4D-_G62XdOImZ7catij{V4D#q2iXHtn&L4ZS;9Q5%W|3SWg z+jWuubz_5&&Wd0e&0XuW%sR@|6Syd|t+RaenLxh->GpPKE08CJ9zZB@6X-zklM}D_ z_U~2}(S7a)iv50*i>h=MM^==?q{d@5gu`5F*}nPOb2;hWmpMMn_syq_&;$3>=j0G) z+jiwwYznt~^&%TM^28QKl34IK@$k)axoIbuaO~brdd_~vNlm}=sPfLG>ZPm>r{?f% zn>Ap^ZPjwk! zY00xs)(I^!1=JDHjJ+t*&D6fN!s!8F+;+g4O-Iou-=|0W)rVXuvSLz#J5L&5=Q0G$ z-6G@8dA_fgff=CEIMbE3_d!mIqR&Isb?pCiRdlkOWX4!Tly;V($`O>sWbe=!%sNy?0!t;-OfY~+sc@8K;OsTDpLaHQLob|d{=TFgd)&RyfN8bkY_}SHp5S)V zEm}`(dg-BoUBoD|%uR8wA^H4ca7Wf?G2Ekp1N9;;HM4?y_6m8QZCP0O+%(Ap_Pj16NIhmNsvX*GJ!}{I6Hd1# z47TsQ)Z`}pY0Nyr<$1c8?;Aqs%!Ataxab}g~N9Wdx#6 z&ql5|oJ^ENDX0yxd$QF=x;{v zjLq$I-b)S>vS+xB;rFS*LgruZ?|e{9+j!rVZQSdgJv6)jDbz4}`!hzLIu*}7jr71& zii?0r5H;fOUkj|>2|MI%cZXO%7$3yxTaflVaQE*uH%TaNYk&KA@~!;=t~>GC=wBydk75np7%55Km+T~S zsHy!|wqxAK3G0R#ockW1@jP_BVjl!MAMR%}CvV_i+qv%7NLOLS<(z!my7&~g_haY7 z2@--~Bwc^MT{%f@*ekxR2b<^J#*5_ba`!R+^7;d;bizcW#Rw&F+x&bEoA(jEnKoYb z_yasvgyvQ}rizh-i7P4rAIM^i4N=21O+iWD#+DI5#Mr;c*rp!W2<>b(AI94jl$0(5=6v%BX z5ux*qc}lagS8&AGSOC0`K@c%Y{ue=U#jiW{1&MPs^H4c0-TUH&kHNYKRV0Zt2#9Q; z$JNGGj7UVzGImg^3#R;H>y<)44SD4N7#YN8dwP_vW%&Bc1@()Mtmr9N%WVQ&++aF` zEA~4QlAz7*Gt|bz9C|{9q_QhIiZqk2-{hjdJXT%#!dI2}8;N%oEmUCQwKnl^8y=F$ve`?9=c^ei?`dT3+z(`FtnWx!I!w@Ap`PC8%v%T&?G z9EBt}EiXg-)i8-T88psFPFP6x+c1ea&%;}2aW(sE*oX!4@#5}Kpt!rcySsGw-ZCwMR&fjUOj)yk#kD5R01kM%3VI@VBf^M@%R%EGL<*u;U}BoP;x9`t8}XbOQ8gLds(M>|bUucCqp=}JV#>{=(2r!5)ZwgweCdkjO_;OokKZAz zZ{<~QD)yV@V`L+3a3gJ({WR+@9X)U5V88##0R%x44exL1TGQHYhJ_T4QUofvb9kDQ zriN<{LF1ajBtZcVN?F}+J-lGh|JRgTlKsD2C!y%w+14`PT=6KFnC~_a_n1bxe~y#c zPqDLx-H89^p{C=B(z|=s z>$D{I^W9W{@UBN?Vy&nZ(fwc_4bT+)_GjbycmC+l#yo45$(cp*u`f2!1BA1wJ6gY$ zb}!3y^70o919;vO*V*0d#5)X2Z}5deY2fWO-c0pL;~x}lmWXWN^DO^tV0W;(E&Gj7 zTu1a=RH+iDPB&M4k11y?aP41WbxZ8UN z7gJeh@^JK?89@0xY*DZm1tV->ccVuFh%-q9KccHHX3M8z8((nBZ_||oNo*|P_88q` z1dsF_$-AAhnNs@}$cZIhcP&_Fzt; z2q)RVm-{ypA-N_aG59C5Sc}Cqs65s>4Vp<6p-st+#2~br;CuX0F{ghRDtKhqsI~xf z!G2ueE`kbBV7Gy=J$^ZhQZ89B4A)}8p|cogR7)nHWlNX8bWQNkY9~mO>AEv-kD@FE z8+81K>rCHDFdstJvS@WfOiYPG!{M6*Q%Me3P?Eq^i+^nNaijV`)C>foE+g2qjQJ9d zjhzU%Z$8N4(wnwLI<rYXrKhQz62>xzwp$8Y{B4I9Y8AX3$v z@<(A3bZn<=nr(!XHp7+5Ytj&(=+m4lQR^XM=L;7Ak7qalp>POOvZ?uCv4I5_F*Dj* zJL6fD>X%mi`60;Z%@m~YDr4T3?J<`)sz2!Ix~qDT>E?*&c9yK+A?+KqQ{ryz8coou zB-5k-u(gz$lMmP@yGf-kezsfuSS(o`UpC%OKXetw%3Xy*D zK>42X?aL33@pwSG_RBVO!w_`bP8qJ|=TsQ!7EsSXoga@Pv7qy+uKx9!wUmsA*Se=T zXdER>C{>u1raZrapuGktk{VTrQogTMg}AgM75oI)NR)XHc?>vYY*VU9>3N8Ht4I_# zhc`Nr{GNrOQOSo}TFv-XBj)Ol0(%Lfqo~t5-;)u|!drYG&5;7Ak{aONhhHG{^_u3; zcfBzS{CM35StyUOcrHOJRBIXEs zOx*GtKQi9u&^7Ess1jPV+xUAhy!-jt>Aeg>jB-qHMTr5!NGAK^SIIO2J@yWYsq_lH zM6RXV2#NPW56^;0QhGCm{-)s#1uh8CD|fp>E|L?LA_%~V3HL7Wd$se`);h>rh|QT* z=}G}OR_@S;VOz4ETjcCW7W36jl^juUhL;ymZGCyFeIDg5DCJa_M<*z;4FFL>tic|D z%cWRTw?4*3{z1U^RA5S$sM{+J?Fq3#JJ<&)*08sdL4Ec6_GbvOC7(jH74BR%Phjg7 z8Yj<_S3ta2RjmdNM!xMsjMpfE!XbfDQm3(xx5QHOcf1OcAlb{7tKS427W9OJ&`Pv|xjZHK*3F&*Pv%BMh|o2V z>6ZtjPA+I%UnT~IElBTe;N)NiH`Y<|-!F)}BRo}Yn{C~Q%Y`p?&w@9k}{@Um;vVwHssaseD|D>G4X+7Pn zS$^7NfvpJ`T(_f8B9ca1um)5RecRolNa$iSp68L&KL`q}HW-*zoy0}`r< z%WznFrmxXyk2OHsgMiDr2fsT~djSkh$l>Uyg^)H~`c=h-EfngG``BM&Xrg%kG9?fsylq@H59dQV@j zd>!Wy#56CaOCpDf&K#nv&IO9aHjka<#Op8LHN~ z|3S20`f%Ds*cMG735eTO=;1Qy{=`!&#n>v6nRBxiSqc9miJC9d=C%WJ^%e!!J=n}C zW|cdO8UL6`IS-)9JkTj#a-juc+Gs(wWgNUKjpVJ1y+5_65l}XBy^r{=$og?hjnW(i zPXm{gle?ZM6^L)}oC2P%JS7a*PD{CI^YwdiG$hMQR01+Nax)ebiwuwK(8;#xlsADt zGw*bNyK-jP4C6Pfs`jSf)7&@1{HBglsbYhGF$!#UX{B{Mjvt-pM`dCjqh%ioyP zAxN$t!@No(zz&wqQOz{OWAdd?61}j5fcSMTty;2cj^ajC$){LSmus^VM6Q=>U1rDu4&f(kmR&@wc6ZoC_!&|Ye|4P+Sxi4vG_s|#nJ8M2jU2T1J z%a7t)kw%*-)ROxTqU@O%R0~F9t{gMpytLx!{V$Ud9r1U$HQ_(0{~*>0L&nf7f;vg< zneB~j>PHU8fsOc%kp@eqM=MEPqB+WR{(8scpi~BrJ%sUjvXpMvSV2XF9xwmZBs{A# zc8?ZWoTQ1#Rpso-@p>!SAK52fZ48ld#`W>+P|G^@{bW&t{L}7!J-5Orl6KxT&1x!m z42c4zk$jf*JoJqW>rs-edo;$-_CScleskImJ~yCfmnWRW)k8PEPq*u7^mQp6=Q$9v z=})3ut=BnzEpi>8HWRzVJ+r=48<}a_RAq}uFLy)jdvrdC^*M^{&ysuPc4CRd$=}$iCe^NBOIp?uBZBABHgS$+sZUM~jQdv!fI2 zii@l=tY8l>ISpspNKTtrgRFV2U-;V#SSEX;7G!kOA{r#zN#-xWS8 z!T-_Z&e=UBRipJ^Cj@YLanKO=(QYcf*+&&7_QM}l@+~qo^IdvG=R38z1pW^Bi{*Sf z{c}1Cwyw>|&e~(aG$~(|!NO(2;NluV0@pz66gi4_76)N=O}fo^{rOrs!sHf5r!S!7 zMwD25=I8W&54*gnHPvC{}wl4{L;6492AjR~J^1%2pp=p`MvRlOlV?d^Q#VT7qp{%%If?jJAaL z_;KKby?nw9H)>Ntx6!~ayAWgg@E2x0Ypz;E@vmPt+IwHNzMf9y8_wM8uUql8zkGKE z$K}kQPNkz`?DlZ*=9TOuel#=-pt#L+sMqd@lX)lxlSm}7c-BY@$42pq>xD0Hw)Usd7 z)JzVKkSQRlT=559hX)9eS(lHUsS=PMDDn@ebv0xtmh|@q`e;M=E9&)#X}Z4;;&xj{ zMSnN~XBOTFC&t!W)D?7kl>t!}4f=#^WGta{!rRCA?+Lpq0p=N#H*RQ7)C zYJ~M7zjP8NJUVAGEKrv~_HEwYlzeM1X|LT9S)CuY2rf6gBkN^mtD>Z^f4OTML{-@YBI)_I-Qd|Q?#Gj)k?;4&hG*@Vo!^_ z)7Hkdr9B5FmxYC6q3fkEvlUl~iAXHzDN*>c{NbKb7(P2U_>G)(nfWqO9#o*k$cW3f zydFm*r&ng?CP!o(#t0^*Of<`q)gA}jj{+)t(~1C^)O@DYALOPXe^JwU2Wo6YGe@2q z0F1*(-g@f$KW0{u*+x!$vPZaaI}dJOzYELs^71L$X+{XrVrnyO$6_Daj4-2R2qVn5 zmoK)hYkz56F6R58ZiwyGwjMwq{Zr0@5e6aHWQ(PqA^N)GvictcSC$|%QJE^131`Q7 z2gGv-sU1p8X8-Or8y`haWW9l?O3rW+bWR_)H}#i@LyJA{mVJ{Od|PD9tkixj>qa7t zB**&pD7~Pix`i^`kdO|qvst{u_wE(oeyoXeP7fthXbZb7vWv7WHdB$;kXawGKLkBz zjT(IGVK9G|z4+zk!_6iXm z^hC-{LA(%5_I6JA?2V@Sy6W=PJp!p?x$Y&_nSMK5cFBTbkMd>b%}g>G*F&-Ow3zGk zbtLD0Ga0<-G=tsk>_mdB4}fy{v!`VCUaBYy>x>a1v;AkV^_tn$pop!oX2wpOOIA)5 z{~eXBL4kSfh-uq(n8{k!W&E#k>%aLjoGrKgGO1{cMr2|{yM9_73N7@tM32{gzs}#V zBe#w&k$eWOpd>qR&INA0%wQekZ6bNPv23HF8_l?Th{L68_gN(R1j0|TDMyK~c36QYs=C+q?y}YYs(?dGo6nt%OzRdqhb~z zIx7T8ImN=On&__1Ok~u)RFk#h`%kU+ts4iWVPKr4;(G(j|IuXkzI| zZ;b45t{WJuZ=hq1a<2wOLgq&Q*ah5`NT-U`$<7?xb#htjby94(BF!?F3>16IGe)e( zHpAIjIZOthA0>U0Nydts@C_5vQIvtxJEQDNbao874?m&pv+0;t_1JQ3ZEd!gP4zqTASbv_UrC-W;`F z{>ZY%tJ5gS<{7H~grO3lDKXoce_bsh?6S?p)yC*E6-GM0(h7thcWDumg@ zM9$u3mJ*oA0qp6`*;!gpzA#aQ*h_s;Ym2ZN$R*buL%jBwJXb90$BVh_CW2A`V&M5I`g@BEa4>Et45O8E z#+w2swj86LRm3d29DZk$l4GhivlJnDEe=oJjqwROMS|Uj5OLwSLVsJ%ho{+yMQtl) zLWWH$I1K8TNqzV^EqC~g=U(vKA$Ui%*TJKa{2g5Z2>8#SiSUv7Bl2A7gVvMvWwE4` zt!hD{NoGZsnQg-Mi`5mBTJC@mgfppwz6k&YFL|s=@VP| zOjZw>w!!HD)3#L3vw`jWwSf$UT#7UHwREcGaa#fk#V)WZrKbeD!2$l7zerXO#cL`B z;A_RcFaRzY=n+=BIp7i0EKAh%%adFC!CPcUFNC_CKuErrh`ZXpRfzvlA4@oji#CoG zpknlNrAcx8GJ@7wHP{(Ry;gZj|+*Vv6YjvLyBYOYy3U>0rMrESs@Cl-COgc9^4_z zF(WTPgB#XCSWlPM8j$Hxl`zizM|-}F#@HsnV*mI_b8p>9E~j46q1+I%!t#qw#PD=rmtgn+Mg=;R_e&NorrDa;7;+TFtq z+xqb5z{ze=j%d075pOWk<_%*TUu)ue_afhvf6xC!aKY_e^uiEQ{W7*#|2mg2W?~<> z8Kd36R!U`GBm;{*&6_1)lkAVLQp*{{H+ufM24Bkl-MrxhCZBs{U+n9g^c26MLiqQB z!Nq5yhK-Ghe-Pc4D`*>Y-4i^P9|Xvsw>OFgB7{!}Ky3d8O`Vce1(|5Boz6x?70^hI zF;u%f+tp)hAqssQV^E|U)SDqF3RuU+Z_V=vbLH2xls1cOIHj%xz3Em5vVmf!?Y&^o*fGltbX zj4}s^8jHft_;rAWhIbNuHKj-L*hhrOds9N^w0v?S)W5pTxRvEh!+iMG}Minb%k3e9^{DWIgek__nR({CZHi^t*HJ! zb|9mf{NZ=WO9OsJvkF%&a}u+PX|&FiKg!(pbztwun3J$cdC6`W^i!rnpKX5XZ}mR{ zvgxX!%!`OdPk?_pp40v5oTM^~RLGj%!}p*=uC2w%QRg37(@)mU5yRK3QbUB!tYBz~ zJ|v;o07D)>$s1LQt&kV_ChHIz-vCjbxlDQ7`O0D|?&hU83+(`i@u+@mFK!WL2nsW) zCKJ^Ck7Tagur^U$yVNm*R3WnBUPJiW8&}eH&xpzWS%2CK`6?aT;O6u^yH8hwmHrE{ z!H6egg)gzoODLK znS(^;szSpdLG)!UmMz1s0D&Ygo#fh5ZztogA~^9QTe?-$%x@#T=f$YmO+e-7s&>7O zx`xP*2j0~f>j2o^@nem8S*aEjZr+?Rib zgD5e`kSmnpR5cBRNRK(5seuq=sL<8x^i%{39`pAHp*vWtZcX6li_b&NICFO$GW7NS zRMs|mv^agj6E{1K-pGUs`=D#_srd(yK!cc8?BAHQG}269{R)Y^XXu7Jd3ioR>kO%kaf8S{h`wJjAvv_45gt&pSh>FclvSVeNPAPJCzR_ zprA|e`HTue?3fSu3_a;a1N0!5k=1u0$MSx2wzv<`K_=xyfCWC4$g*??{!t{)+pKVO zM7KYGN2vsoe?Fa)l$=VRk6 zHz{;D`&pD@)jknh@nK^b`BJzktLzlrohkVZPBM0gN_7t1tGG11Fp1+}A5x6ahpD!0 z-J~BWBMvK9x)InGU1qQ&exO%6aBzz3D3J9D@GO?9suou*x19hgoBFy2=Q-RaH|oTw zE){N7Y!`5!Rt_!bRf+q#D~fVCcOa~75!Daa6m~W)U!|R_cyMzfk=8KYimNgj1ROfo zGIzcayBR4h*0d|+_{~;9S?eW0sz6<9m1)2YKVf4YdO;a17-x!6pe2NN+Vmr{q&qlH z@LNl-B*4^S{sw-+Hg>%GyWYe_gA$ zoU0s;Oev1zfq$NE%3O(Y)!s%iEo-KW?aXXX`QotYR7=p?l1d@?Mz3REz;lFI!0x+qxXH)X%KlHDS_J9u^zXxkE*DAitt*?NWC?=C%6M~VoYh>pu1xUu4r zaM_QO!{+5<25q%!M-`deM=93Y^h-S*@TF+=gTUQQQ$8z!TOCeTTI-#x*u=7YA?YR8 zOxqT2`u?$>5fzu0;LOhg^b-O|^Qb*b>se9AWK8z?d!3W=C%dem4hmwq&>$q^@QMH@ zc{Qu=CApMNmX(hn9ghXeC4&aFoYL7~xiiT%o9l1m>Iv4E=vj_VuT!BZ=kL%% z8+S+fN>2Z1OLH=Js)P5vJ0if+h2e^JB+nRSRnbcF@ZRwVzs9mqze*h|wn%CvRw-)j zg$&)TVzw+EG+1OKVPO05GqtOxQ>iTb7YkZDoUu@pX|si~Y?>Iwr>TKpnIKTER17`@ zEc|m4Pr87`kaI%I&S!qZvj-*czZftPC+qy53Wg&ur?;D>3S@RsCQfBUnlq}HCCN{zvKU4Hid-RNJC zTLpX{L-x$2|JKkLC-<9MA*UShLOv{zK2&nJ3rVqJD2(?FeE+0nBwR?vM#+qlaAjb>e3-qfO zH7Spi;fkD|@)f^#-nZCT<}-?D*Vm5TFk{Itc;}IEET--2Q{8_d&yE=b5IDx0AAEa*}T1 zBerpN97pV!H4?4Nl*~|rvEbG1IKBv5mpG5D8*ItgP7l!9^)-yjnVT236icIQIIQu_ zX2QhbZK{}9ZW&w;j1t3jA6aPnHZg5^ccQyiedMF{BB_w8(WLDIXf9I5Q767oC`sEt zel%%S#CU0$t#ZGmI82p!`PGx}Rp9Hz79FWtpb!W9ZR*S6An}dj8do2SSl=wE)^<2g zPHE1xyd$lrhm`0^P~{@c;y6bS`uJEi3qZdf%Hm)yzjM24xf$GQY&yp0R&!Wr#rD^t zi#mt!u}Nc4$h!OOdv35wtGFuF?=K^H_4PXWv7h`!g@>&V#zdFeDxD-)VS?~r*#w$t zJWjqPCIg8w92n$#J4T@TCQjfRqFa$TLP6p>g+qGP^oExt5}I56zY4@ernwYt`~8SK7fHb}7d{p4*ts3(G=%;1t}4%l_~ z`N64(#W(Uma%9m;Y3q-}HW&p;^POaFpM>wA*EIC<@bZzGtZOux z>1=U`AB)%qA7O-T7hj)agYy+7!K62PWqLW-G;+*w^qM$&`_S+UpR_^eEOB+5_K(`8 zjzOor60WM_m|rmCe)tz@u^3Ep^)^HW?3uDuEd6XGcwtRaC_UGkT#0_wAu1&z&Fx4O zMDb@T^rSL>>zmS0^#-|&QD&9F z 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 literal 0 HcmV?d00001 diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-logo.svg b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-logo.svg new file mode 100644 index 00000000..c5624526 --- /dev/null +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/templates/quote/uecko-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts index 64922ed6..804249db 100644 --- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts @@ -1,6 +1,10 @@ import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core"; -import type { IMapperRegistry, ModuleParams } from "@erp/core/api"; -import { InMemoryMapperRegistry, SequelizeTransactionManager } from "@erp/core/api"; +import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api"; +import { + InMemoryMapperRegistry, + InMemoryPresenterRegistry, + SequelizeTransactionManager, +} from "@erp/core/api"; import { CreateCustomerInvoiceAssembler, CreateCustomerInvoiceUseCase, @@ -9,6 +13,7 @@ import { GetCustomerInvoiceUseCase, ListCustomerInvoicesAssembler, ListCustomerInvoicesUseCase, + ReportCustomerInvoiceUseCase, UpdateCustomerInvoiceAssembler, UpdateCustomerInvoiceUseCase, } from "../application"; @@ -24,7 +29,7 @@ type InvoiceDeps = { catalogs: { taxes: JsonTaxCatalogProvider; }; - assemblers: { + presenters: { list: ListCustomerInvoicesAssembler; get: GetCustomerInvoiceAssembler; create: CreateCustomerInvoiceAssembler; @@ -36,16 +41,16 @@ type InvoiceDeps = { create: () => CreateCustomerInvoiceUseCase; update: () => UpdateCustomerInvoiceUseCase; delete: () => DeleteCustomerInvoiceUseCase; - }; - presenters: { - // list: (res: Response) => ListPresenter; + report: () => ReportCustomerInvoiceUseCase; }; }; -let _repo: CustomerInvoiceRepository | null = null; +let _presenterRegistry: IPresenterRegistry | null = null; let _mapperRegistry: IMapperRegistry | null = null; + +let _repo: CustomerInvoiceRepository | null = null; let _service: CustomerInvoiceService | null = null; -let _assemblers: InvoiceDeps["assemblers"] | null = null; +const _presenters: InvoiceDeps["presenters"] | null = null; let _catalogs: InvoiceDeps["catalogs"] | null = null; export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { @@ -53,26 +58,28 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { const transactionManager = new SequelizeTransactionManager(database); if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider }; - const fullMapper: CustomerInvoiceFullMapper = new CustomerInvoiceFullMapper({ - taxCatalog: _catalogs!.taxes, - }); - const listMapper = new CustomerInvoiceListMapper(); - if (!_mapperRegistry) { _mapperRegistry = new InMemoryMapperRegistry(); - _mapperRegistry.registerDomainMapper("FULL", fullMapper); - _mapperRegistry.registerReadModelMapper("LIST", listMapper); + _mapperRegistry.registerDomainMapper( + "FULL", + new CustomerInvoiceFullMapper({ + taxCatalog: _catalogs!.taxes, + }) + ); + _mapperRegistry.registerReadModelMapper("LIST", new CustomerInvoiceListMapper()); } if (!_repo) _repo = new CustomerInvoiceRepository({ mapperRegistry: _mapperRegistry, database }); if (!_service) _service = new CustomerInvoiceService(_repo); - if (!_assemblers) { - _assemblers = { + if (!_presenterRegistry) { + _presenterRegistry = new InMemoryPresenterRegistry(); + _presenterRegistry.registerPresenter(key, mapper); + /*_presenters = { list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO create: new CreateCustomerInvoiceAssembler(), // transforma domain → CreatedDTO update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO - }; + };*/ } return { @@ -80,31 +87,29 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { repo: _repo, mapperRegistry: _mapperRegistry, service: _service, - assemblers: _assemblers, + presenters: _presenters, catalogs: _catalogs, build: { list: () => - new ListCustomerInvoicesUseCase(_service!, transactionManager!, _assemblers!.list), - get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get), + new ListCustomerInvoicesUseCase(_service!, transactionManager!, _presenters!.list), + get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _presenters!.get), create: () => new CreateCustomerInvoiceUseCase( _service!, transactionManager!, - _assemblers!.create, + _presenters!.create, _catalogs!.taxes ), update: () => new UpdateCustomerInvoiceUseCase( _service!, transactionManager!, - _assemblers!.update, + _presenters!.update, _catalogs!.taxes ), delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!), - }, - presenters: { - //list: (res: Response) => createListPresenter(res), - //json: (res: Response, status: number = 200) => createJsonPresenter(res, status), + report: () => + new ReportCustomerInvoiceUseCase(_service!, transactionManager!, _presenters!.get), }, }; } diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts new file mode 100644 index 00000000..5b1984f3 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts @@ -0,0 +1,25 @@ +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase as ReportCustomerInvoiceUseCase } from "../../../application"; + +export class ReportCustomerInvoiceController extends ExpressController { + public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) { + super(); + // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query + this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + } + + protected async executeImpl() { + const companyId = this.getTenantId()!; // garantizado por tenantGuard + const { invoice_id } = this.req.params; + + const getUseCase = getUsecaasdasd; + const invoiceDto = await + + const result = await this.useCase.execute({ invoice_id, companyId, }); + + return result.match( + (data) => this.downloadPDF(result.data), + (err) => this.handleError(err) + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts index afe13678..708ae8e5 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts @@ -7,6 +7,7 @@ import { CustomerInvoiceListRequestSchema, DeleteCustomerInvoiceByIdRequestSchema, GetCustomerInvoiceByIdRequestSchema, + ReportCustomerInvoiceByIdRequestSchema, } from "../../../common/dto"; import { getInvoiceDependencies } from "../dependencies"; import { @@ -104,5 +105,16 @@ export const customerInvoicesRouter = (params: ModuleParams) => { } ); + router.get( + "/:invoice_id/report", + //checkTabContext, + validateRequest(ReportCustomerInvoiceByIdRequestSchema, "params"), + (req: Request, res: Response, next: NextFunction) => { + const useCase = deps.build.report(); + const controller = new ReportCustomerInvoiceController(useCase); + return controller.execute(req, res, next); + } + ); + app.use(`${baseRoutePath}/customer-invoices`, router); }; diff --git a/packages/rdx-utils/src/helpers/id-utils.ts b/packages/rdx-utils/src/helpers/id-utils.ts index 1204494b..efe232a2 100644 --- a/packages/rdx-utils/src/helpers/id-utils.ts +++ b/packages/rdx-utils/src/helpers/id-utils.ts @@ -1,3 +1,4 @@ -import { v4 as uuidv4 } from "uuid"; +import { v4 as uuidv4, v7 as uuidv7 } from "uuid"; export const generateUUIDv4 = (): string => uuidv4(); +export const generateUUIDv7 = (): string => uuidv7();