sunat-engine-nest 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,186 +5,428 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
8
14
  Object.defineProperty(exports, "__esModule", { value: true });
9
15
  exports.DocumentPdfService = void 0;
10
16
  const common_1 = require("@nestjs/common");
11
17
  const PdfMake = require("pdfmake/build/pdfmake");
12
18
  const PdfFonts = require("pdfmake/build/vfs_fonts");
13
19
  const QRCode = require("qrcode");
20
+ const constants_1 = require("../constants");
21
+ const pdf_license_service_1 = require("../licensing/pdf-license.service");
14
22
  PdfMake.vfs = PdfFonts.pdfMake?.vfs ?? PdfFonts.vfs;
23
+ const DEFAULT_BRAND_COLOR = '#1e40af';
15
24
  const PAGE_SIZES = {
16
25
  A4: [595.28, 841.89],
17
26
  TICKET_80MM: [226.77, 650],
18
27
  STICKER_A6: [419.53, 595.28],
19
28
  };
20
29
  let DocumentPdfService = class DocumentPdfService {
21
- async generateInvoice(payload, format = 'A4') {
22
- const qrContent = this.buildQrContent(payload);
23
- const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 120, margin: 1 });
30
+ constructor(options = {}, license = new pdf_license_service_1.PdfLicenseService()) {
31
+ this.options = options;
32
+ this.license = license;
33
+ }
34
+ async generateInvoice(payload, format = 'A4', opts) {
35
+ this.license.assertValid(opts?.licenseKey ?? this.options?.pdf?.licenseKey);
36
+ const branding = this.resolveBranding(opts?.branding);
37
+ const qrDataUrl = await this.qr(this.buildQrContent(payload));
24
38
  const docType = payload.tipoDoc === '01' ? 'FACTURA ELECTRÓNICA' : 'BOLETA DE VENTA ELECTRÓNICA';
25
39
  const docId = `${payload.serie}-${payload.correlativo}`;
26
40
  if (format === 'A4') {
27
- return this.generateA4(payload, docType, docId, qrDataUrl);
41
+ return this.generateInvoiceA4(payload, docType, docId, qrDataUrl, branding);
28
42
  }
29
- else {
30
- return this.generateTicket(payload, docType, docId, qrDataUrl, format);
43
+ return this.generateInvoiceTicket(payload, docType, docId, qrDataUrl, format);
44
+ }
45
+ async generateInvoiceA4(payload, docType, docId, qrDataUrl, branding) {
46
+ const docDefinition = {
47
+ pageSize: 'A4',
48
+ pageMargins: [40, 40, 40, 40],
49
+ content: [
50
+ ...this.buildHeader(payload.company, docType, docId, branding),
51
+ {
52
+ table: {
53
+ widths: ['auto', '*'],
54
+ body: [
55
+ [{ text: 'Señor(es):', bold: true }, payload.client.rznSocial],
56
+ [{ text: 'Tipo / N° Doc:', bold: true }, `${payload.client.tipoDoc} / ${payload.client.numDoc}`],
57
+ [{ text: 'Fecha Emisión:', bold: true }, payload.fechaEmision.slice(0, 10)],
58
+ [{ text: 'Moneda:', bold: true }, payload.tipoMoneda],
59
+ ],
60
+ },
61
+ layout: this.cardLayout(),
62
+ margin: [0, 0, 0, 12],
63
+ },
64
+ this.buildItemsTable(payload.details, branding),
65
+ this.buildTotalsQrBlock(payload, qrDataUrl, branding, 'TOTAL A PAGAR'),
66
+ ...(payload.legends ?? []).map((l) => ({ text: l.value, style: 'legend', margin: [0, 8, 0, 0] })),
67
+ ],
68
+ styles: this.baseStyles(),
69
+ defaultStyle: { fontSize: 9 },
70
+ };
71
+ return this.renderPdf(docDefinition);
72
+ }
73
+ async generateInvoiceTicket(payload, docType, docId, qrDataUrl, format) {
74
+ const moneda = payload.tipoMoneda;
75
+ const isSticker = format === 'STICKER_A6';
76
+ const docDefinition = {
77
+ pageSize: { width: PAGE_SIZES[format][0], height: 'auto' },
78
+ pageMargins: isSticker ? [20, 20, 20, 20] : [10, 10, 10, 10],
79
+ content: [
80
+ ...this.ticketHeader(payload.company, docType, docId, isSticker),
81
+ { text: `Cliente: ${payload.client.rznSocial}`, fontSize: 7 },
82
+ { text: `Doc: ${payload.client.numDoc}`, fontSize: 7 },
83
+ { text: `Fecha: ${payload.fechaEmision.slice(0, 10)}`, fontSize: 7 },
84
+ this.ticketDivider(),
85
+ ...payload.details.map((d) => this.ticketDetailRow(d)),
86
+ this.ticketDivider(),
87
+ ...(payload.mtoOperGravadas ? [this.totRow('OP. GRAVADAS', `${moneda} ${this.fmt(payload.mtoOperGravadas)}`)] : []),
88
+ this.totRow('IGV 18%', `${moneda} ${this.fmt(payload.mtoIGV)}`),
89
+ this.totRow('TOTAL', `${moneda} ${this.fmt(payload.mtoImpVenta)}`, true),
90
+ ...(payload.legends ?? []).map((l) => ({ text: l.value, fontSize: 7, italics: true, margin: [0, 1, 0, 0] })),
91
+ { image: qrDataUrl, width: isSticker ? 80 : 60, alignment: 'center', margin: [0, 6, 0, 4] },
92
+ { text: 'Comprobante Electrónico - SUNAT', fontSize: 6, alignment: 'center', color: '#888' },
93
+ ],
94
+ defaultStyle: { fontSize: 8 },
95
+ };
96
+ return this.renderPdf(docDefinition);
97
+ }
98
+ async generateNote(payload, format = 'A4', opts) {
99
+ this.license.assertValid(opts?.licenseKey ?? this.options?.pdf?.licenseKey);
100
+ const branding = this.resolveBranding(opts?.branding);
101
+ const qrDataUrl = await this.qr(this.buildQrContent(payload));
102
+ const docType = payload.tipoDoc === '07' ? 'NOTA DE CRÉDITO ELECTRÓNICA' : 'NOTA DE DÉBITO ELECTRÓNICA';
103
+ const docId = `${payload.serie}-${payload.correlativo}`;
104
+ if (format === 'A4') {
105
+ return this.generateNoteA4(payload, docType, docId, qrDataUrl, branding);
31
106
  }
107
+ return this.generateNoteTicket(payload, docType, docId, qrDataUrl, format);
32
108
  }
33
- async generateA4(payload, docType, docId, qrDataUrl) {
109
+ async generateNoteA4(payload, docType, docId, qrDataUrl, branding) {
110
+ const motivoLabel = payload.tipoDoc === '07' ? 'Motivo (N. Crédito):' : 'Motivo (N. Débito):';
111
+ const docDefinition = {
112
+ pageSize: 'A4',
113
+ pageMargins: [40, 40, 40, 40],
114
+ content: [
115
+ ...this.buildHeader(payload.company, docType, docId, branding),
116
+ {
117
+ table: {
118
+ widths: ['auto', '*'],
119
+ body: [
120
+ [{ text: 'Señor(es):', bold: true }, payload.client.rznSocial],
121
+ [{ text: 'Tipo / N° Doc:', bold: true }, `${payload.client.tipoDoc} / ${payload.client.numDoc}`],
122
+ [{ text: 'Fecha Emisión:', bold: true }, payload.fechaEmision.slice(0, 10)],
123
+ [{ text: 'Moneda:', bold: true }, payload.tipoMoneda],
124
+ [{ text: 'Doc. Afectado:', bold: true }, `${payload.tipDocAfectado} ${payload.numDocAfectado}`],
125
+ [{ text: motivoLabel, bold: true }, payload.desMotivo],
126
+ ],
127
+ },
128
+ layout: this.cardLayout(),
129
+ margin: [0, 0, 0, 12],
130
+ },
131
+ this.buildItemsTable(payload.details, branding),
132
+ this.buildTotalsQrBlock(payload, qrDataUrl, branding, 'TOTAL'),
133
+ ...(payload.legends ?? []).map((l) => ({ text: l.value, style: 'legend', margin: [0, 8, 0, 0] })),
134
+ ],
135
+ styles: this.baseStyles(),
136
+ defaultStyle: { fontSize: 9 },
137
+ };
138
+ return this.renderPdf(docDefinition);
139
+ }
140
+ async generateNoteTicket(payload, docType, docId, qrDataUrl, format) {
34
141
  const moneda = payload.tipoMoneda;
35
- const details = payload.details.map((d, i) => [
142
+ const isSticker = format === 'STICKER_A6';
143
+ const docDefinition = {
144
+ pageSize: { width: PAGE_SIZES[format][0], height: 'auto' },
145
+ pageMargins: isSticker ? [20, 20, 20, 20] : [10, 10, 10, 10],
146
+ content: [
147
+ ...this.ticketHeader(payload.company, docType, docId, isSticker),
148
+ { text: `Afecta a: ${payload.tipDocAfectado} ${payload.numDocAfectado}`, alignment: 'center', fontSize: 7 },
149
+ this.ticketDivider(),
150
+ { text: `Cliente: ${payload.client.rznSocial}`, fontSize: 7 },
151
+ { text: `Doc: ${payload.client.numDoc}`, fontSize: 7 },
152
+ { text: `Fecha: ${payload.fechaEmision.slice(0, 10)}`, fontSize: 7 },
153
+ { text: `Motivo: ${payload.desMotivo}`, fontSize: 7 },
154
+ this.ticketDivider(),
155
+ ...payload.details.map((d) => this.ticketDetailRow(d)),
156
+ this.ticketDivider(),
157
+ ...(payload.mtoOperGravadas ? [this.totRow('OP. GRAVADAS', `${moneda} ${this.fmt(payload.mtoOperGravadas)}`)] : []),
158
+ this.totRow('IGV 18%', `${moneda} ${this.fmt(payload.mtoIGV)}`),
159
+ this.totRow('TOTAL', `${moneda} ${this.fmt(payload.mtoImpVenta)}`, true),
160
+ ...(payload.legends ?? []).map((l) => ({ text: l.value, fontSize: 7, italics: true, margin: [0, 1, 0, 0] })),
161
+ { image: qrDataUrl, width: isSticker ? 80 : 60, alignment: 'center', margin: [0, 6, 0, 4] },
162
+ { text: 'Comprobante Electrónico - SUNAT', fontSize: 6, alignment: 'center', color: '#888' },
163
+ ],
164
+ defaultStyle: { fontSize: 8 },
165
+ };
166
+ return this.renderPdf(docDefinition);
167
+ }
168
+ async generateDespatch(payload, opts) {
169
+ this.license.assertValid(opts?.licenseKey ?? this.options?.pdf?.licenseKey);
170
+ const branding = this.resolveBranding(opts?.branding);
171
+ const docId = `${payload.serie}-${payload.correlativo}`;
172
+ const qrDataUrl = await this.qr([payload.company.ruc, payload.tipoDoc, payload.serie, payload.correlativo].join('|'));
173
+ const envio = payload.envio;
174
+ const modalidad = envio.modTraslado === '01' ? 'Transporte Público' : 'Transporte Privado';
175
+ const chofer = envio.choferes?.[0];
176
+ const detailRows = payload.details.map((d, i) => [
36
177
  { text: String(i + 1), alignment: 'center' },
37
178
  { text: d.unidad },
38
179
  { text: String(d.cantidad), alignment: 'right' },
39
180
  { text: d.descripcion },
40
- { text: this.fmt(d.mtoValorUnitario), alignment: 'right' },
41
- { text: this.fmt(d.mtoValorVenta), alignment: 'right' },
42
181
  ]);
43
182
  const docDefinition = {
44
183
  pageSize: 'A4',
45
184
  pageMargins: [40, 40, 40, 40],
46
185
  content: [
186
+ ...this.buildHeader(payload.company, 'GUÍA DE REMISIÓN ELECTRÓNICA', docId, branding),
187
+ {
188
+ table: {
189
+ widths: ['auto', '*'],
190
+ body: [
191
+ [{ text: 'Destinatario:', bold: true }, payload.destinatario.rznSocial],
192
+ [{ text: 'Tipo / N° Doc:', bold: true }, `${payload.destinatario.tipoDoc} / ${payload.destinatario.numDoc}`],
193
+ [{ text: 'Fecha Emisión:', bold: true }, payload.fechaEmision.slice(0, 10)],
194
+ ],
195
+ },
196
+ layout: this.cardLayout(),
197
+ margin: [0, 0, 0, 10],
198
+ },
199
+ {
200
+ table: {
201
+ widths: ['auto', '*', 'auto', '*'],
202
+ body: [
203
+ [{ text: 'Motivo:', bold: true }, envio.desTraslado, { text: 'Modalidad:', bold: true }, modalidad],
204
+ [{ text: 'F. Traslado:', bold: true }, (envio.fecTraslado ?? '').slice(0, 10), { text: 'Peso Total:', bold: true }, `${envio.pesoTotal} ${envio.undPesoTotal}`],
205
+ [{ text: 'N° Bultos:', bold: true }, String(envio.numBultos ?? '-'), '', ''],
206
+ ],
207
+ },
208
+ layout: this.cardLayout(),
209
+ margin: [0, 0, 0, 10],
210
+ },
47
211
  {
48
212
  columns: [
49
213
  {
50
- width: '60%',
214
+ width: '50%',
51
215
  stack: [
52
- { text: payload.company.razonSocial, style: 'companyName' },
53
- { text: payload.company.nombreComercial ?? '', style: 'companyComercial' },
54
- { text: `RUC: ${payload.company.ruc}`, style: 'small' },
55
- { text: payload.company.address?.direccion ?? '', style: 'small' },
216
+ { text: 'PUNTO DE PARTIDA', style: 'blockLabel', color: branding.color },
217
+ { text: envio.partida.direccion, style: 'small' },
56
218
  ],
57
219
  },
58
220
  {
59
- width: '40%',
60
- alignment: 'center',
221
+ width: '50%',
61
222
  stack: [
62
- { text: 'R.U.C. ' + payload.company.ruc, bold: true, margin: [0, 0, 0, 4] },
63
- { text: docType, style: 'docType' },
64
- { text: docId, style: 'docId' },
223
+ { text: 'PUNTO DE LLEGADA', style: 'blockLabel', color: branding.color },
224
+ { text: envio.llegada.direccion, style: 'small' },
65
225
  ],
66
- border: [true, true, true, true],
67
- fillColor: '#f8f8f8',
68
- margin: [4, 0, 0, 0],
69
226
  },
70
227
  ],
71
- },
72
- { canvas: [{ type: 'line', x1: 0, y1: 5, x2: 515, y2: 5, lineWidth: 1 }], margin: [0, 8, 0, 8] },
73
- {
74
- table: {
75
- widths: ['auto', '*'],
76
- body: [
77
- [{ text: 'Señor(es):', bold: true }, payload.client.rznSocial],
78
- [{ text: 'Tipo / N° Doc:', bold: true }, `${payload.client.tipoDoc} / ${payload.client.numDoc}`],
79
- [{ text: 'Fecha Emisión:', bold: true }, payload.fechaEmision.slice(0, 10)],
80
- [{ text: 'Moneda:', bold: true }, moneda],
81
- ],
82
- },
83
- layout: 'noBorders',
84
228
  margin: [0, 0, 0, 10],
85
229
  },
230
+ ...(envio.transportista || envio.vehiculo || chofer ? [{
231
+ table: {
232
+ widths: ['auto', '*'],
233
+ body: [
234
+ ...(envio.transportista ? [[{ text: 'Transportista:', bold: true }, `${envio.transportista.rznSocial} (${envio.transportista.numDoc})`]] : []),
235
+ ...(envio.vehiculo ? [[{ text: 'Vehículo:', bold: true }, envio.vehiculo.placa]] : []),
236
+ ...(chofer ? [[{ text: 'Conductor:', bold: true }, `${chofer.nombres ?? ''} ${chofer.apellidos ?? ''} - Lic. ${chofer.licencia}`]] : []),
237
+ ],
238
+ },
239
+ layout: this.cardLayout(),
240
+ margin: [0, 0, 0, 10],
241
+ }] : []),
86
242
  {
87
243
  table: {
88
244
  headerRows: 1,
89
- widths: [20, 35, 40, '*', 60, 60],
245
+ widths: [20, 40, 40, '*'],
90
246
  body: [
91
247
  [
92
248
  { text: '#', style: 'tableHeader' },
93
249
  { text: 'U.M.', style: 'tableHeader' },
94
250
  { text: 'Cant.', style: 'tableHeader', alignment: 'right' },
95
251
  { text: 'Descripción', style: 'tableHeader' },
96
- { text: 'P.Unit.', style: 'tableHeader', alignment: 'right' },
97
- { text: 'Total', style: 'tableHeader', alignment: 'right' },
98
252
  ],
99
- ...details,
253
+ ...detailRows,
100
254
  ],
101
255
  },
102
- margin: [0, 0, 0, 10],
256
+ layout: this.zebraTableLayout(branding),
257
+ margin: [0, 0, 0, 12],
103
258
  },
104
259
  {
105
- columns: [
106
- {
107
- width: '50%',
108
- stack: [
109
- { image: qrDataUrl, width: 100, margin: [0, 10, 0, 4] },
110
- { text: 'Representación impresa del comprobante electrónico', style: 'tiny', italics: true },
111
- ],
112
- },
113
- {
114
- width: '50%',
115
- table: {
116
- widths: ['*', 70],
117
- body: [
118
- ...(payload.mtoOperGravadas ? [
119
- [{ text: 'OP. GRAVADAS', alignment: 'right' }, { text: `${moneda} ${this.fmt(payload.mtoOperGravadas)}`, alignment: 'right' }],
120
- ] : []),
121
- ...(payload.mtoOperExoneradas ? [
122
- [{ text: 'OP. EXONERADAS', alignment: 'right' }, { text: `${moneda} ${this.fmt(payload.mtoOperExoneradas)}`, alignment: 'right' }],
123
- ] : []),
124
- ...(payload.mtoOperInafectas ? [
125
- [{ text: 'OP. INAFECTAS', alignment: 'right' }, { text: `${moneda} ${this.fmt(payload.mtoOperInafectas)}`, alignment: 'right' }],
126
- ] : []),
127
- [{ text: 'IGV (18%)', alignment: 'right' }, { text: `${moneda} ${this.fmt(payload.mtoIGV)}`, alignment: 'right' }],
128
- [{ text: 'TOTAL A PAGAR', bold: true, alignment: 'right' }, { text: `${moneda} ${this.fmt(payload.mtoImpVenta)}`, bold: true, alignment: 'right' }],
129
- ],
130
- },
131
- layout: 'noBorders',
132
- margin: [0, 10, 0, 0],
133
- },
260
+ stack: [
261
+ { image: qrDataUrl, width: 85, alignment: 'center', margin: [0, 4, 0, 4] },
262
+ { text: 'Representación impresa de la Guía de Remisión Electrónica', style: 'tiny', italics: true, alignment: 'center' },
134
263
  ],
135
264
  },
136
- ...(payload.legends ?? []).map((l) => ({ text: l.value, style: 'legend' })),
137
265
  ],
138
266
  styles: {
139
- companyName: { fontSize: 14, bold: true, margin: [0, 0, 0, 2] },
140
- companyComercial: { fontSize: 10, color: '#555', margin: [0, 0, 0, 2] },
141
- docType: { fontSize: 10, bold: true, margin: [0, 8, 0, 4] },
142
- docId: { fontSize: 14, bold: true, color: '#c00' },
143
- tableHeader: { bold: true, fillColor: '#eeeeee', fontSize: 9 },
144
- small: { fontSize: 9, color: '#444' },
145
- tiny: { fontSize: 7, color: '#888' },
146
- legend: { fontSize: 9, italics: true, margin: [0, 2, 0, 0] },
267
+ ...this.baseStyles(),
268
+ blockLabel: { bold: true, fontSize: 8, margin: [0, 0, 0, 3] },
147
269
  },
148
270
  defaultStyle: { fontSize: 9 },
149
271
  };
150
272
  return this.renderPdf(docDefinition);
151
273
  }
152
- async generateTicket(payload, docType, docId, qrDataUrl, format) {
153
- const moneda = payload.tipoMoneda;
154
- const isSticker = format === 'STICKER_A6';
155
- const docDefinition = {
156
- pageSize: { width: PAGE_SIZES[format][0], height: 'auto' },
157
- pageMargins: isSticker ? [20, 20, 20, 20] : [10, 10, 10, 10],
158
- content: [
159
- { text: payload.company.razonSocial, bold: true, alignment: 'center', fontSize: isSticker ? 11 : 9 },
160
- { text: `RUC: ${payload.company.ruc}`, alignment: 'center', fontSize: 8 },
161
- { text: payload.company.address?.direccion ?? '', alignment: 'center', fontSize: 7 },
162
- { canvas: [{ type: 'line', x1: 0, y1: 2, x2: 200, y2: 2, lineWidth: 0.5, dash: { length: 3 } }], margin: [0, 3, 0, 3] },
163
- { text: docType, alignment: 'center', bold: true, fontSize: 9 },
164
- { text: docId, alignment: 'center', bold: true, fontSize: 11, color: '#c00' },
165
- { canvas: [{ type: 'line', x1: 0, y1: 2, x2: 200, y2: 2, lineWidth: 0.5, dash: { length: 3 } }], margin: [0, 3, 0, 3] },
166
- { text: `Cliente: ${payload.client.rznSocial}`, fontSize: 7 },
167
- { text: `Doc: ${payload.client.numDoc}`, fontSize: 7 },
168
- { text: `Fecha: ${payload.fechaEmision.slice(0, 10)}`, fontSize: 7 },
169
- { canvas: [{ type: 'line', x1: 0, y1: 2, x2: 200, y2: 2, lineWidth: 0.5 }], margin: [0, 3, 0, 3] },
170
- ...payload.details.map((d) => ({
171
- columns: [
172
- { text: `${d.descripcion}\n${d.cantidad} ${d.unidad} x ${this.fmt(d.mtoValorUnitario)}`, width: '*', fontSize: 7 },
173
- { text: this.fmt(d.mtoValorVenta), width: 45, alignment: 'right', fontSize: 7 },
274
+ buildHeader(company, docType, docId, branding) {
275
+ const brandTint = this.lighten(branding.color, 0.92);
276
+ return [
277
+ {
278
+ columns: [
279
+ ...(branding.logoBase64 ? [{ image: branding.logoBase64, width: 64, height: 64, fit: [64, 64] }] : []),
280
+ {
281
+ width: branding.logoBase64 ? '48%' : '60%',
282
+ stack: [
283
+ { text: company.razonSocial, style: 'companyName' },
284
+ { text: company.nombreComercial ?? '', style: 'companyComercial' },
285
+ { text: `RUC: ${company.ruc}`, style: 'small' },
286
+ { text: company.address?.direccion ?? '', style: 'small' },
287
+ ],
288
+ margin: branding.logoBase64 ? [10, 0, 0, 0] : [0, 0, 0, 0],
289
+ },
290
+ {
291
+ width: '40%',
292
+ alignment: 'center',
293
+ stack: [
294
+ { text: 'R.U.C. ' + company.ruc, bold: true, margin: [0, 0, 0, 4] },
295
+ { text: docType, style: 'docType' },
296
+ { text: docId, bold: true, fontSize: 14, color: branding.color },
297
+ ],
298
+ border: [true, true, true, true],
299
+ borderColor: [branding.color, branding.color, branding.color, branding.color],
300
+ fillColor: brandTint,
301
+ margin: [4, 6, 0, 6],
302
+ },
303
+ ],
304
+ },
305
+ { canvas: [{ type: 'line', x1: 0, y1: 4, x2: 515, y2: 4, lineWidth: 2, lineColor: branding.color }], margin: [0, 10, 0, 10] },
306
+ ];
307
+ }
308
+ buildItemsTable(details, branding) {
309
+ const rows = details.map((d, i) => [
310
+ { text: String(i + 1), alignment: 'center' },
311
+ { text: d.unidad },
312
+ { text: String(d.cantidad), alignment: 'right' },
313
+ { text: d.descripcion },
314
+ { text: this.fmt(d.mtoValorUnitario), alignment: 'right' },
315
+ { text: this.fmt(d.mtoValorVenta), alignment: 'right' },
316
+ ]);
317
+ return {
318
+ table: {
319
+ headerRows: 1,
320
+ widths: [20, 35, 40, '*', 60, 60],
321
+ body: [
322
+ [
323
+ { text: '#', style: 'tableHeader' },
324
+ { text: 'U.M.', style: 'tableHeader' },
325
+ { text: 'Cant.', style: 'tableHeader', alignment: 'right' },
326
+ { text: 'Descripción', style: 'tableHeader' },
327
+ { text: 'P.Unit.', style: 'tableHeader', alignment: 'right' },
328
+ { text: 'Total', style: 'tableHeader', alignment: 'right' },
174
329
  ],
175
- margin: [0, 1, 0, 1],
176
- })),
177
- { canvas: [{ type: 'line', x1: 0, y1: 2, x2: 200, y2: 2, lineWidth: 0.5 }], margin: [0, 3, 0, 3] },
178
- ...(payload.mtoOperGravadas ? [this.totRow('OP. GRAVADAS', `${moneda} ${this.fmt(payload.mtoOperGravadas)}`)] : []),
179
- this.totRow('IGV 18%', `${moneda} ${this.fmt(payload.mtoIGV)}`),
180
- this.totRow('TOTAL', `${moneda} ${this.fmt(payload.mtoImpVenta)}`, true),
181
- ...(payload.legends ?? []).map((l) => ({ text: l.value, fontSize: 7, italics: true, margin: [0, 1, 0, 0] })),
182
- { image: qrDataUrl, width: isSticker ? 80 : 60, alignment: 'center', margin: [0, 6, 0, 4] },
183
- { text: 'Comprobante Electrónico - SUNAT', fontSize: 6, alignment: 'center', color: '#888' },
330
+ ...rows,
331
+ ],
332
+ },
333
+ layout: this.zebraTableLayout(branding),
334
+ margin: [0, 0, 0, 12],
335
+ };
336
+ }
337
+ buildTotalsQrBlock(totals, qrDataUrl, branding, totalLabel) {
338
+ const moneda = totals.tipoMoneda;
339
+ const brandTint = this.lighten(branding.color, 0.92);
340
+ const totalRows = [
341
+ ...(totals.mtoOperGravadas ? [[{ text: 'OP. GRAVADAS', alignment: 'right' }, { text: `${moneda} ${this.fmt(totals.mtoOperGravadas)}`, alignment: 'right' }]] : []),
342
+ ...(totals.mtoOperExoneradas ? [[{ text: 'OP. EXONERADAS', alignment: 'right' }, { text: `${moneda} ${this.fmt(totals.mtoOperExoneradas)}`, alignment: 'right' }]] : []),
343
+ ...(totals.mtoOperInafectas ? [[{ text: 'OP. INAFECTAS', alignment: 'right' }, { text: `${moneda} ${this.fmt(totals.mtoOperInafectas)}`, alignment: 'right' }]] : []),
344
+ [{ text: 'IGV (18%)', alignment: 'right' }, { text: `${moneda} ${this.fmt(totals.mtoIGV)}`, alignment: 'right' }],
345
+ [
346
+ { text: totalLabel, bold: true, alignment: 'right', fontSize: 10, fillColor: brandTint, margin: [0, 3, 4, 3] },
347
+ { text: `${moneda} ${this.fmt(totals.mtoImpVenta)}`, bold: true, alignment: 'right', fontSize: 10, color: branding.color, fillColor: brandTint, margin: [0, 3, 4, 3] },
348
+ ],
349
+ ];
350
+ return {
351
+ columns: [
352
+ {
353
+ width: '38%',
354
+ stack: [
355
+ { image: qrDataUrl, width: 85, margin: [0, 4, 0, 4] },
356
+ { text: 'Representación impresa del comprobante electrónico', style: 'tiny', italics: true },
357
+ ],
358
+ },
359
+ {
360
+ width: '62%',
361
+ table: { widths: ['*', 80], body: totalRows },
362
+ layout: 'noBorders',
363
+ },
184
364
  ],
185
- defaultStyle: { fontSize: 8 },
186
365
  };
187
- return this.renderPdf(docDefinition);
366
+ }
367
+ cardLayout() {
368
+ return {
369
+ hLineWidth: () => 0,
370
+ vLineWidth: () => 0,
371
+ paddingLeft: () => 6,
372
+ paddingRight: () => 6,
373
+ paddingTop: () => 3,
374
+ paddingBottom: () => 3,
375
+ fillColor: () => '#f7f8fa',
376
+ };
377
+ }
378
+ zebraTableLayout(branding) {
379
+ return {
380
+ hLineWidth: (i, node) => (i === 0 || i === node.table.body.length) ? 0 : 0.5,
381
+ vLineWidth: () => 0,
382
+ hLineColor: () => '#e5e7eb',
383
+ paddingTop: () => 4,
384
+ paddingBottom: () => 4,
385
+ fillColor: (rowIndex) => rowIndex === 0 ? branding.color : (rowIndex % 2 === 0 ? '#f7f8fa' : null),
386
+ };
387
+ }
388
+ baseStyles() {
389
+ return {
390
+ companyName: { fontSize: 14, bold: true, color: '#1a1a1a', margin: [0, 0, 0, 2] },
391
+ companyComercial: { fontSize: 10, color: '#555', margin: [0, 0, 0, 2] },
392
+ docType: { fontSize: 10, bold: true, margin: [0, 8, 0, 4] },
393
+ tableHeader: { bold: true, color: '#ffffff', fontSize: 9 },
394
+ small: { fontSize: 9, color: '#444' },
395
+ tiny: { fontSize: 7, color: '#888' },
396
+ legend: { fontSize: 9, italics: true },
397
+ };
398
+ }
399
+ resolveBranding(branding) {
400
+ return {
401
+ logoBase64: branding?.logoBase64 ?? '',
402
+ color: branding?.color ?? DEFAULT_BRAND_COLOR,
403
+ };
404
+ }
405
+ ticketHeader(company, docType, docId, isSticker) {
406
+ return [
407
+ { text: company.razonSocial, bold: true, alignment: 'center', fontSize: isSticker ? 11 : 9 },
408
+ { text: `RUC: ${company.ruc}`, alignment: 'center', fontSize: 8 },
409
+ { text: company.address?.direccion ?? '', alignment: 'center', fontSize: 7 },
410
+ this.ticketDivider(true),
411
+ { text: docType, alignment: 'center', bold: true, fontSize: 9 },
412
+ { text: docId, alignment: 'center', bold: true, fontSize: 11, color: '#c00' },
413
+ this.ticketDivider(true),
414
+ ];
415
+ }
416
+ ticketDivider(dashed = false) {
417
+ return {
418
+ canvas: [{ type: 'line', x1: 0, y1: 2, x2: 200, y2: 2, lineWidth: 0.5, ...(dashed ? { dash: { length: 3 } } : {}) }],
419
+ margin: [0, 3, 0, 3],
420
+ };
421
+ }
422
+ ticketDetailRow(d) {
423
+ return {
424
+ columns: [
425
+ { text: `${d.descripcion}\n${d.cantidad} ${d.unidad} x ${this.fmt(d.mtoValorUnitario)}`, width: '*', fontSize: 7 },
426
+ { text: this.fmt(d.mtoValorVenta), width: 45, alignment: 'right', fontSize: 7 },
427
+ ],
428
+ margin: [0, 1, 0, 1],
429
+ };
188
430
  }
189
431
  totRow(label, value, bold = false) {
190
432
  return {
@@ -209,9 +451,20 @@ let DocumentPdfService = class DocumentPdfService {
209
451
  '',
210
452
  ].join('|');
211
453
  }
454
+ async qr(content) {
455
+ return QRCode.toDataURL(content, { width: 120, margin: 1 });
456
+ }
212
457
  fmt(n) {
213
458
  return (n ?? 0).toFixed(2);
214
459
  }
460
+ lighten(hex, amount) {
461
+ const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
462
+ if (!m)
463
+ return hex;
464
+ const mix = (c) => Math.round(c + (255 - c) * amount);
465
+ const [r, g, b] = [m[1], m[2], m[3]].map((h) => mix(parseInt(h, 16)));
466
+ return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
467
+ }
215
468
  renderPdf(docDefinition) {
216
469
  return new Promise((resolve, reject) => {
217
470
  const pdfDoc = PdfMake.createPdf(docDefinition);
@@ -225,6 +478,9 @@ let DocumentPdfService = class DocumentPdfService {
225
478
  };
226
479
  exports.DocumentPdfService = DocumentPdfService;
227
480
  exports.DocumentPdfService = DocumentPdfService = __decorate([
228
- (0, common_1.Injectable)()
481
+ (0, common_1.Injectable)(),
482
+ __param(0, (0, common_1.Optional)()),
483
+ __param(0, (0, common_1.Inject)(constants_1.SUNAT_ENGINE_OPTIONS)),
484
+ __metadata("design:paramtypes", [Object, pdf_license_service_1.PdfLicenseService])
229
485
  ], DocumentPdfService);
230
486
  //# sourceMappingURL=document-pdf.service.js.map