709 lines
21 KiB
TypeScript
709 lines
21 KiB
TypeScript
import {
|
|
EntityNotFoundError,
|
|
InfrastructureRepositoryError,
|
|
SequelizeRepository,
|
|
translateSequelizeError,
|
|
} from "@erp/core/api";
|
|
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
|
import type { UniqueID } from "@repo/rdx-ddd";
|
|
import { type Collection, Result } from "@repo/rdx-utils";
|
|
import type { FindOptions, InferAttributes, OrderItem, Transaction } from "sequelize";
|
|
|
|
import type {
|
|
CustomerInvoice,
|
|
CustomerInvoiceStatus,
|
|
ICustomerInvoiceRepository,
|
|
} from "../../domain";
|
|
import type {
|
|
CustomerInvoiceListDTO,
|
|
ICustomerInvoiceDomainMapper,
|
|
ICustomerInvoiceListMapper,
|
|
} from "../mappers";
|
|
|
|
import { CustomerInvoiceModel } from "./models/customer-invoice.model";
|
|
import { CustomerInvoiceItemModel } from "./models/customer-invoice-item.model";
|
|
import { CustomerInvoiceItemTaxModel } from "./models/customer-invoice-item-tax.model";
|
|
import { CustomerInvoiceTaxModel } from "./models/customer-invoice-tax.model";
|
|
import { VerifactuRecordModel } from "./models/verifactu-record.model";
|
|
|
|
export class CustomerInvoiceRepository
|
|
extends SequelizeRepository<CustomerInvoice>
|
|
implements ICustomerInvoiceRepository
|
|
{
|
|
// Listado por tenant con criteria saneada
|
|
/* async searchInCompany(criteria: any, companyId: string): Promise<{
|
|
rows: InvoiceListRow[];
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
}> {
|
|
const { where, order, limit, offset, attributes } = sanitizeListCriteria(criteria);
|
|
|
|
// WHERE con scope de company
|
|
const scopedWhere = { ...where, company_id: companyId };
|
|
|
|
const options: FindAndCountOptions = {
|
|
where: scopedWhere,
|
|
order,
|
|
limit,
|
|
offset,
|
|
attributes,
|
|
raw: true, // devolvemos objetos planos -> más rápido
|
|
nest: false,
|
|
distinct: true // por si en el futuro añadimos includes no duplicar count
|
|
};
|
|
|
|
const { rows, count } = await CustomerInvoiceModel.findAndCountAll(options);
|
|
|
|
return {
|
|
rows: rows as unknown as InvoiceListRow[],
|
|
total: typeof count === "number" ? count : (count as any[]).length,
|
|
limit,
|
|
offset,
|
|
};
|
|
} */
|
|
|
|
/**
|
|
*
|
|
* Crea una nueva factura.
|
|
*
|
|
* @param invoice - El agregado a guardar.
|
|
* @param transaction - Transacción activa para la operación.
|
|
* @returns Result<void, Error>
|
|
*/
|
|
async create(invoice: CustomerInvoice, transaction?: Transaction): Promise<Result<void, Error>> {
|
|
try {
|
|
const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({
|
|
resource: "customer-invoice",
|
|
});
|
|
const dtoResult = mapper.mapToPersistence(invoice);
|
|
|
|
if (dtoResult.isFailure) {
|
|
return Result.fail(dtoResult.error);
|
|
}
|
|
|
|
const dto = dtoResult.data;
|
|
const { id, items, taxes, verifactu, ...createPayload } = dto;
|
|
|
|
// 1. Insertar cabecera
|
|
await CustomerInvoiceModel.create(
|
|
{
|
|
...createPayload,
|
|
id,
|
|
},
|
|
{ transaction }
|
|
);
|
|
|
|
// 2. Inserta taxes de cabecera
|
|
if (Array.isArray(taxes) && taxes.length > 0) {
|
|
await CustomerInvoiceTaxModel.bulkCreate(taxes, { transaction });
|
|
}
|
|
|
|
// 3. Inserta items + sus taxes
|
|
if (Array.isArray(items) && items.length > 0) {
|
|
for (const item of items) {
|
|
const { taxes: itemTaxes, ...itemData } = item;
|
|
await CustomerInvoiceItemModel.create(itemData, { transaction });
|
|
|
|
if (Array.isArray(itemTaxes) && itemTaxes.length > 0) {
|
|
await CustomerInvoiceItemTaxModel.bulkCreate(itemTaxes, { transaction });
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Inserta VerifactuRecord si existe
|
|
if (verifactu) {
|
|
await VerifactuRecordModel.create(verifactu, { transaction });
|
|
}
|
|
|
|
return Result.ok();
|
|
} catch (err: unknown) {
|
|
return Result.fail(translateSequelizeError(err));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actualiza una factura existente.
|
|
*
|
|
* @param invoice - El agregado a actualizar.
|
|
* @param transaction - Transacción activa para la operación.
|
|
* @returns Result<void, Error>
|
|
*/
|
|
async update(invoice: CustomerInvoice, transaction: Transaction): Promise<Result<void, Error>> {
|
|
try {
|
|
const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({
|
|
resource: "customer-invoice",
|
|
});
|
|
const dtoResult = mapper.mapToPersistence(invoice);
|
|
|
|
if (dtoResult.isFailure) return Result.fail(dtoResult.error);
|
|
|
|
const dto = dtoResult.data;
|
|
const { id, items, taxes, ...updatePayload } = dto;
|
|
|
|
// 1. Actualizar cabecera
|
|
const [affectedCount] = await CustomerInvoiceModel.update(updatePayload, {
|
|
where: { id /*, version */ },
|
|
transaction,
|
|
});
|
|
|
|
if (affectedCount === 0) {
|
|
return Result.fail(
|
|
new InfrastructureRepositoryError(`Invoice ${id} not found or concurrency issue`)
|
|
);
|
|
}
|
|
|
|
// 2. Borra items y taxes previos (simplifica sincronización)
|
|
await CustomerInvoiceItemModel.destroy({
|
|
where: { invoice_id: id },
|
|
transaction,
|
|
});
|
|
await CustomerInvoiceTaxModel.destroy({
|
|
where: { invoice_id: id },
|
|
transaction,
|
|
});
|
|
|
|
// 3. Inserta taxes de cabecera
|
|
if (Array.isArray(taxes) && taxes.length > 0) {
|
|
await CustomerInvoiceTaxModel.bulkCreate(taxes, { transaction });
|
|
}
|
|
|
|
// 4. Inserta items + sus taxes
|
|
if (Array.isArray(items) && items.length > 0) {
|
|
for (const item of items) {
|
|
const { taxes: itemTaxes, ...itemData } = item;
|
|
await CustomerInvoiceItemModel.create(itemData, { transaction });
|
|
|
|
if (Array.isArray(itemTaxes) && itemTaxes.length > 0) {
|
|
await CustomerInvoiceItemTaxModel.bulkCreate(itemTaxes, { transaction });
|
|
}
|
|
}
|
|
}
|
|
|
|
return Result.ok();
|
|
} catch (err: unknown) {
|
|
return Result.fail(translateSequelizeError(err));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Comprueba si existe una factura con un `id` dentro de una `company`.
|
|
*
|
|
* @param companyId - Identificador UUID de la empresa a la que pertenece la factura.
|
|
* @param id - Identificador UUID de la factura.
|
|
* @param transaction - Transacción activa para la operación.
|
|
* @param options - Opciones adicionales para la consulta (Sequelize FindOptions)
|
|
* @returns Result<boolean, Error>
|
|
*/
|
|
async existsByIdInCompany(
|
|
companyId: UniqueID,
|
|
id: UniqueID,
|
|
transaction: Transaction,
|
|
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
|
): Promise<Result<boolean, Error>> {
|
|
try {
|
|
const count = await CustomerInvoiceModel.count({
|
|
...options,
|
|
where: {
|
|
id: id.toString(),
|
|
company_id: companyId.toString(),
|
|
...(options.where ?? {}),
|
|
},
|
|
transaction,
|
|
});
|
|
return Result.ok(Boolean(count > 0));
|
|
} catch (error: unknown) {
|
|
return Result.fail(translateSequelizeError(error));
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Busca una proforma por su identificador único.
|
|
*
|
|
* @param companyId - Identificador UUID de la empresa a la que pertenece la proforma.
|
|
* @param id - UUID de la proforma.
|
|
* @param transaction - Transacción activa para la operación.
|
|
* @param options - Opciones adicionales para la consulta (Sequelize FindOptions)
|
|
* @returns Result<CustomerInvoice, Error>
|
|
*/
|
|
async getProformaByIdInCompany(
|
|
companyId: UniqueID,
|
|
id: UniqueID,
|
|
transaction: Transaction,
|
|
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
|
): Promise<Result<CustomerInvoice, Error>> {
|
|
const { CustomerModel } = this._database.models;
|
|
|
|
try {
|
|
const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({
|
|
resource: "customer-invoice",
|
|
});
|
|
|
|
// Normalización defensiva de order/include
|
|
const normalizedOrder = Array.isArray(options.order)
|
|
? options.order
|
|
: options.order
|
|
? [options.order]
|
|
: [];
|
|
|
|
const normalizedInclude = Array.isArray(options.include)
|
|
? options.include
|
|
: options.include
|
|
? [options.include]
|
|
: [];
|
|
|
|
const mergedOptions: FindOptions<InferAttributes<CustomerInvoiceModel>> = {
|
|
...options,
|
|
where: {
|
|
...(options.where ?? {}),
|
|
id: id.toString(),
|
|
is_proforma: true,
|
|
company_id: companyId.toString(),
|
|
},
|
|
order: [
|
|
...normalizedOrder,
|
|
[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"],
|
|
],
|
|
include: [
|
|
...normalizedInclude,
|
|
{
|
|
model: CustomerModel,
|
|
as: "current_customer",
|
|
required: false,
|
|
},
|
|
{
|
|
model: CustomerInvoiceItemModel,
|
|
as: "items",
|
|
required: false,
|
|
include: [
|
|
{
|
|
model: CustomerInvoiceItemTaxModel,
|
|
as: "taxes",
|
|
required: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
model: CustomerInvoiceTaxModel,
|
|
as: "taxes",
|
|
required: false,
|
|
},
|
|
],
|
|
transaction,
|
|
};
|
|
|
|
const row = await CustomerInvoiceModel.findOne(mergedOptions);
|
|
|
|
if (!row) {
|
|
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
|
|
}
|
|
|
|
const invoice = mapper.mapToDomain(row);
|
|
return invoice;
|
|
} catch (err: unknown) {
|
|
return Result.fail(translateSequelizeError(err));
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Busca una factura por su identificador único.
|
|
*
|
|
* @param companyId - Identificador UUID de la empresa a la que pertenece la factura.
|
|
* @param id - UUID de la factura.
|
|
* @param transaction - Transacción activa para la operación.
|
|
* @param options - Opciones adicionales para la consulta (Sequelize FindOptions)
|
|
* @returns Result<CustomerInvoice, Error>
|
|
*/
|
|
async getIssuedInvoiceByIdInCompany(
|
|
companyId: UniqueID,
|
|
id: UniqueID,
|
|
transaction: Transaction,
|
|
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
|
): Promise<Result<CustomerInvoice, Error>> {
|
|
const { CustomerModel } = this._database.models;
|
|
|
|
try {
|
|
const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({
|
|
resource: "customer-invoice",
|
|
});
|
|
|
|
// Normalización defensiva de order/include
|
|
const normalizedOrder = Array.isArray(options.order)
|
|
? options.order
|
|
: options.order
|
|
? [options.order]
|
|
: [];
|
|
|
|
const normalizedInclude = Array.isArray(options.include)
|
|
? options.include
|
|
: options.include
|
|
? [options.include]
|
|
: [];
|
|
|
|
const mergedOptions: FindOptions<InferAttributes<CustomerInvoiceModel>> = {
|
|
...options,
|
|
where: {
|
|
...(options.where ?? {}),
|
|
id: id.toString(),
|
|
is_proforma: false,
|
|
company_id: companyId.toString(),
|
|
},
|
|
order: [
|
|
...normalizedOrder,
|
|
[{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"],
|
|
],
|
|
include: [
|
|
...normalizedInclude,
|
|
{
|
|
model: VerifactuRecordModel,
|
|
as: "verifactu",
|
|
required: false,
|
|
attributes: ["id", "estado", "url", "uuid"],
|
|
},
|
|
{
|
|
model: CustomerModel,
|
|
as: "current_customer",
|
|
required: false,
|
|
},
|
|
{
|
|
model: CustomerInvoiceItemModel,
|
|
as: "items",
|
|
required: false,
|
|
include: [
|
|
{
|
|
model: CustomerInvoiceItemTaxModel,
|
|
as: "taxes",
|
|
required: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
model: CustomerInvoiceTaxModel,
|
|
as: "taxes",
|
|
required: false,
|
|
},
|
|
],
|
|
transaction,
|
|
};
|
|
|
|
const row = await CustomerInvoiceModel.findOne(mergedOptions);
|
|
|
|
if (!row) {
|
|
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
|
|
}
|
|
|
|
const invoice = mapper.mapToDomain(row);
|
|
return invoice;
|
|
} catch (err: unknown) {
|
|
return Result.fail(translateSequelizeError(err));
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Consulta proformas usando un objeto Criteria (filtros, orden, paginación).
|
|
*
|
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
|
* @param criteria - Criterios de búsqueda.
|
|
* @param transaction - Transacción activa para la operación.
|
|
* @returns Result<CustomerInvoice[], Error>
|
|
*
|
|
* @see Criteria
|
|
*/
|
|
public async findProformasByCriteriaInCompany(
|
|
companyId: UniqueID,
|
|
criteria: Criteria,
|
|
transaction: Transaction,
|
|
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
|
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
|
const { CustomerModel } = this._database.models;
|
|
|
|
try {
|
|
const mapper: ICustomerInvoiceListMapper = this._registry.getQueryMapper({
|
|
resource: "customer-invoice",
|
|
query: "LIST",
|
|
});
|
|
|
|
const converter = new CriteriaToSequelizeConverter();
|
|
const query = converter.convert(criteria, {
|
|
searchableFields: ["invoice_number", "reference", "description"],
|
|
mappings: {
|
|
reference: "CustomerInvoiceModel.reference",
|
|
},
|
|
allowedFields: ["invoice_date", "id", "created_at"],
|
|
enableFullText: true,
|
|
database: this._database,
|
|
strictMode: true, // fuerza error si ORDER BY no permitido
|
|
});
|
|
|
|
// Normalización defensiva de order/include
|
|
const normalizedOrder = Array.isArray(options.order)
|
|
? options.order
|
|
: options.order
|
|
? [options.order]
|
|
: [];
|
|
|
|
const normalizedInclude = Array.isArray(options.include)
|
|
? options.include
|
|
: options.include
|
|
? [options.include]
|
|
: [];
|
|
|
|
query.where = {
|
|
...query.where,
|
|
...(options.where ?? {}),
|
|
is_proforma: true,
|
|
company_id: companyId.toString(),
|
|
deleted_at: null,
|
|
};
|
|
|
|
query.order = [...(query.order as OrderItem[]), ...normalizedOrder];
|
|
|
|
query.include = [
|
|
...normalizedInclude,
|
|
{
|
|
model: CustomerModel,
|
|
as: "current_customer",
|
|
required: false, // false => LEFT JOIN
|
|
attributes: [
|
|
"name",
|
|
"trade_name",
|
|
"tin",
|
|
"street",
|
|
"street2",
|
|
"city",
|
|
"postal_code",
|
|
"province",
|
|
"country",
|
|
],
|
|
},
|
|
{
|
|
model: CustomerInvoiceTaxModel,
|
|
as: "taxes",
|
|
required: false,
|
|
separate: true, // => query aparte, devuelve siempre array
|
|
attributes: ["tax_id", "tax_code"],
|
|
},
|
|
];
|
|
|
|
// Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento)
|
|
/*const { rows, count } = await CustomerInvoiceModel.findAndCountAll({
|
|
...query,
|
|
transaction,
|
|
});*/
|
|
|
|
const [rows, count] = await Promise.all([
|
|
CustomerInvoiceModel.findAll({
|
|
...query,
|
|
transaction,
|
|
}),
|
|
CustomerInvoiceModel.count({
|
|
where: query.where,
|
|
distinct: true, // evita duplicados por LEFT JOIN
|
|
transaction,
|
|
}),
|
|
]);
|
|
|
|
return mapper.mapToDTOCollection(rows, count);
|
|
} catch (err: unknown) {
|
|
return Result.fail(translateSequelizeError(err));
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Consulta facturas usando un objeto Criteria (filtros, orden, paginación).
|
|
*
|
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
|
* @param criteria - Criterios de búsqueda.
|
|
* @param transaction - Transacción activa para la operación.
|
|
* @returns Result<CustomerInvoice[], Error>
|
|
*
|
|
* @see Criteria
|
|
*/
|
|
public async findIssuedInvoicesByCriteriaInCompany(
|
|
companyId: UniqueID,
|
|
criteria: Criteria,
|
|
transaction: Transaction,
|
|
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
|
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
|
const { CustomerModel } = this._database.models;
|
|
|
|
try {
|
|
const mapper: ICustomerInvoiceListMapper = this._registry.getQueryMapper({
|
|
resource: "customer-invoice",
|
|
query: "LIST",
|
|
});
|
|
|
|
const converter = new CriteriaToSequelizeConverter();
|
|
const query = converter.convert(criteria, {
|
|
searchableFields: ["invoice_number", "reference", "description"],
|
|
mappings: {
|
|
reference: "CustomerInvoiceModel.reference",
|
|
},
|
|
allowedFields: ["invoice_date", "id", "created_at"],
|
|
enableFullText: true,
|
|
database: this._database,
|
|
strictMode: true, // fuerza error si ORDER BY no permitido
|
|
});
|
|
|
|
// Normalización defensiva de order/include
|
|
const normalizedOrder = Array.isArray(options.order)
|
|
? options.order
|
|
: options.order
|
|
? [options.order]
|
|
: [];
|
|
|
|
const normalizedInclude = Array.isArray(options.include)
|
|
? options.include
|
|
: options.include
|
|
? [options.include]
|
|
: [];
|
|
|
|
query.where = {
|
|
...query.where,
|
|
...(options.where ?? {}),
|
|
is_proforma: false,
|
|
company_id: companyId.toString(),
|
|
deleted_at: null,
|
|
};
|
|
|
|
query.order = [...(query.order as OrderItem[]), ...normalizedOrder];
|
|
|
|
query.include = [
|
|
...normalizedInclude,
|
|
{
|
|
model: VerifactuRecordModel,
|
|
as: "verifactu",
|
|
required: false,
|
|
attributes: ["id", "estado", "url", "uuid"],
|
|
},
|
|
{
|
|
model: CustomerModel,
|
|
as: "current_customer",
|
|
required: false, // false => LEFT JOIN
|
|
attributes: [
|
|
"name",
|
|
"trade_name",
|
|
"tin",
|
|
"street",
|
|
"street2",
|
|
"city",
|
|
"postal_code",
|
|
"province",
|
|
"country",
|
|
],
|
|
},
|
|
{
|
|
model: CustomerInvoiceTaxModel,
|
|
as: "taxes",
|
|
required: false,
|
|
separate: true, // => query aparte, devuelve siempre array
|
|
attributes: ["tax_id", "tax_code"],
|
|
},
|
|
];
|
|
|
|
// Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento)
|
|
/*const { rows, count } = await CustomerInvoiceModel.findAndCountAll({
|
|
...query,
|
|
transaction,
|
|
});*/
|
|
|
|
const [rows, count] = await Promise.all([
|
|
CustomerInvoiceModel.findAll({
|
|
...query,
|
|
transaction,
|
|
}),
|
|
CustomerInvoiceModel.count({
|
|
where: query.where,
|
|
distinct: true, // evita duplicados por LEFT JOIN
|
|
transaction,
|
|
}),
|
|
]);
|
|
|
|
return mapper.mapToDTOCollection(rows, count);
|
|
} catch (err: unknown) {
|
|
return Result.fail(translateSequelizeError(err));
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Elimina o marca como eliminada una proforma dentro de una empresa.
|
|
*
|
|
* @param companyId - ID de la empresa.
|
|
* @param id - UUID de la proforma a eliminar.
|
|
* @param transaction - Transacción activa para la operación.
|
|
* @returns Result<void, Error>
|
|
*/
|
|
async deleteProformaByIdInCompany(
|
|
companyId: UniqueID,
|
|
id: UniqueID,
|
|
transaction: Transaction
|
|
): Promise<Result<boolean, Error>> {
|
|
try {
|
|
const deleted = await CustomerInvoiceModel.destroy({
|
|
where: {
|
|
id: id.toString(),
|
|
company_id: companyId.toString(),
|
|
is_proforma: true,
|
|
},
|
|
transaction,
|
|
});
|
|
|
|
if (deleted === 0) {
|
|
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
|
|
}
|
|
|
|
return Result.ok(true);
|
|
} catch (err: unknown) {
|
|
return Result.fail(translateSequelizeError(err));
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Actualiza el "status" de una proforma
|
|
*
|
|
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
|
* @param id - UUID de la factura a eliminar.
|
|
* @param newStatus - nuevo estado
|
|
* @param transaction - Transacción activa para la operación.
|
|
* @returns Result<boolean, Error>
|
|
*/
|
|
async updateProformaStatusByIdInCompany(
|
|
companyId: UniqueID,
|
|
id: UniqueID,
|
|
newStatus: CustomerInvoiceStatus,
|
|
transaction: Transaction
|
|
): Promise<Result<boolean, Error>> {
|
|
try {
|
|
const [affected] = await CustomerInvoiceModel.update(
|
|
{
|
|
status: newStatus.toPrimitive(),
|
|
},
|
|
{
|
|
where: { id: id.toPrimitive(), company_id: companyId.toPrimitive() },
|
|
fields: ["status"],
|
|
transaction,
|
|
individualHooks: true,
|
|
}
|
|
);
|
|
|
|
if (affected === 0) {
|
|
return Result.fail(
|
|
new InfrastructureRepositoryError(
|
|
"Concurrency conflict or not found update customer invoice"
|
|
)
|
|
);
|
|
}
|
|
|
|
return Result.ok(true);
|
|
} catch (err: unknown) {
|
|
return Result.fail(translateSequelizeError(err));
|
|
}
|
|
}
|
|
}
|