Facturas de cliente

This commit is contained in:
David Arranz 2025-09-18 13:17:18 +02:00
parent c8db3099a4
commit 1919a54dc2
35 changed files with 127 additions and 70 deletions

View File

@ -85,7 +85,7 @@ export class CreateAccountUseCase {
website: dto.website ? Maybe.some(dto.website) : Maybe.none(), website: dto.website ? Maybe.some(dto.website) : Maybe.none(),
legalRecord: dto.legal_record, legalRecord: dto.legal_record,
defaultTax: dto.default_tax, defaultTax: dto.default_tax,
langCode: dto.lang_code, langCode: dto.language_code,
currencyCode: dto.currency_code, currencyCode: dto.currency_code,
logo: dto.logo ? Maybe.some(dto.logo) : Maybe.none(), logo: dto.logo ? Maybe.some(dto.logo) : Maybe.none(),
}; };

View File

@ -106,8 +106,8 @@ export class UpdateAccountUseCase {
validatedData.defaultTax = dto.default_tax; validatedData.defaultTax = dto.default_tax;
} }
if (dto.lang_code) { if (dto.language_code) {
validatedData.langCode = dto.lang_code; validatedData.langCode = dto.language_code;
} }
if (dto.currency_code) { if (dto.currency_code) {

View File

@ -38,7 +38,7 @@ const sampleAccountPrimitives = {
legal_record: "Registro Mercantil XYZ", legal_record: "Registro Mercantil XYZ",
default_tax: 21, default_tax: 21,
status: "active", status: "active",
lang_code: "es", language_code: "es",
currency_code: "EUR", currency_code: "EUR",
logo: "https://xyz.com/logo.png", logo: "https://xyz.com/logo.png",
}; };
@ -87,7 +87,7 @@ const accountBuilder = (accountData: any) => {
: Maybe.none(), : Maybe.none(),
legalRecord: sampleAccountPrimitives.legal_record, legalRecord: sampleAccountPrimitives.legal_record,
defaultTax: sampleAccountPrimitives.default_tax, defaultTax: sampleAccountPrimitives.default_tax,
langCode: sampleAccountPrimitives.lang_code, langCode: sampleAccountPrimitives.language_code,
currencyCode: sampleAccountPrimitives.currency_code, currencyCode: sampleAccountPrimitives.currency_code,
logo: sampleAccountPrimitives.logo ? Maybe.some(sampleAccountPrimitives.logo) : Maybe.none(), logo: sampleAccountPrimitives.logo ? Maybe.some(sampleAccountPrimitives.logo) : Maybe.none(),
}; };

View File

@ -30,7 +30,7 @@ const sampleAccount = {
legal_record: "Registro Mercantil XYZ", legal_record: "Registro Mercantil XYZ",
default_tax: 21, default_tax: 21,
status: "active", status: "active",
lang_code: "es", language_code: "es",
currency_code: "EUR", currency_code: "EUR",
logo: "https://xyz.com/logo.png", logo: "https://xyz.com/logo.png",
}; };

View File

@ -64,7 +64,7 @@ export class AccountMapper
website: source.website ? Maybe.some(source.website) : Maybe.none(), website: source.website ? Maybe.some(source.website) : Maybe.none(),
legalRecord: source.legal_record, legalRecord: source.legal_record,
defaultTax: source.default_tax, defaultTax: source.default_tax,
langCode: source.lang_code, langCode: source.language_code,
currencyCode: source.currency_code, currencyCode: source.currency_code,
logo: source.logo ? Maybe.some(source.logo) : Maybe.none(), logo: source.logo ? Maybe.some(source.logo) : Maybe.none(),
}, },
@ -94,7 +94,7 @@ export class AccountMapper
legal_record: source.legalRecord, legal_record: source.legalRecord,
default_tax: source.defaultTax, default_tax: source.defaultTax,
status: source.isActive ? "active" : "inactive", status: source.isActive ? "active" : "inactive",
lang_code: source.langCode, language_code: source.langCode,
currency_code: source.currencyCode, currency_code: source.currencyCode,
logo: source.logo.getOrUndefined(), logo: source.logo.getOrUndefined(),
}; };

View File

@ -37,7 +37,7 @@ export class AccountModel extends Model<InferAttributes<AccountModel>, AccountCr
declare default_tax: number; declare default_tax: number;
declare status: string; declare status: string;
declare lang_code: string; declare language_code: string;
declare currency_code: string; declare currency_code: string;
declare logo: CreationOptional<string>; declare logo: CreationOptional<string>;
} }
@ -129,7 +129,7 @@ export default (sequelize: Sequelize) => {
defaultValue: null, defaultValue: null,
}, },
lang_code: { language_code: {
type: DataTypes.STRING(2), type: DataTypes.STRING(2),
allowNull: false, allowNull: false,
defaultValue: "es", defaultValue: "es",

View File

@ -30,7 +30,7 @@ export const createAccountPresenter: ICreateAccountPresenter = {
default_tax: ensureNumber(account.defaultTax), default_tax: ensureNumber(account.defaultTax),
status: ensureString(account.isActive ? "active" : "inactive"), status: ensureString(account.isActive ? "active" : "inactive"),
lang_code: ensureString(account.langCode), language_code: ensureString(account.langCode),
currency_code: ensureString(account.currencyCode), currency_code: ensureString(account.currencyCode),
logo: ensureString(account.logo.getOrUndefined()), logo: ensureString(account.logo.getOrUndefined()),
}), }),

View File

@ -30,7 +30,7 @@ export const getAccountPresenter: IGetAccountPresenter = {
default_tax: ensureNumber(account.defaultTax), default_tax: ensureNumber(account.defaultTax),
status: ensureString(account.isActive ? "active" : "inactive"), status: ensureString(account.isActive ? "active" : "inactive"),
lang_code: ensureString(account.langCode), language_code: ensureString(account.langCode),
currency_code: ensureString(account.currencyCode), currency_code: ensureString(account.currencyCode),
logo: ensureString(account.logo.getOrUndefined()), logo: ensureString(account.logo.getOrUndefined()),
}), }),

View File

@ -31,7 +31,7 @@ export const listAccountsPresenter: IListAccountsPresenter = {
default_tax: ensureNumber(account.defaultTax), default_tax: ensureNumber(account.defaultTax),
status: ensureString(account.isActive ? "active" : "inactive"), status: ensureString(account.isActive ? "active" : "inactive"),
lang_code: ensureString(account.langCode), language_code: ensureString(account.langCode),
currency_code: ensureString(account.currencyCode), currency_code: ensureString(account.currencyCode),
logo: ensureString(account.logo.getOrUndefined()), logo: ensureString(account.logo.getOrUndefined()),
})), })),

View File

@ -30,7 +30,7 @@ export const updateAccountPresenter: IUpdateAccountPresenter = {
default_tax: ensureNumber(account.defaultTax), default_tax: ensureNumber(account.defaultTax),
status: ensureString(account.isActive ? "active" : "inactive"), status: ensureString(account.isActive ? "active" : "inactive"),
lang_code: ensureString(account.langCode), language_code: ensureString(account.langCode),
currency_code: ensureString(account.currencyCode), currency_code: ensureString(account.currencyCode),
logo: ensureString(account.logo.getOrUndefined()), logo: ensureString(account.logo.getOrUndefined()),
}), }),

View File

@ -21,7 +21,7 @@ export interface ICreateAccountRequestDTO {
legal_record: string; legal_record: string;
default_tax: number; default_tax: number;
lang_code: string; language_code: string;
currency_code: string; currency_code: string;
logo: string; logo: string;
} }
@ -46,7 +46,7 @@ export interface IUpdateAccountRequestDTO {
legal_record: string; legal_record: string;
default_tax: number; default_tax: number;
lang_code: string; language_code: string;
currency_code: string; currency_code: string;
logo: string; logo: string;
} }

View File

@ -21,7 +21,7 @@ export interface IListAccountsResponseDTO {
default_tax: number; default_tax: number;
status: string; status: string;
lang_code: string; language_code: string;
currency_code: string; currency_code: string;
logo: string; logo: string;
} }
@ -49,7 +49,7 @@ export interface IGetAccountResponseDTO {
default_tax: number; default_tax: number;
status: string; status: string;
lang_code: string; language_code: string;
currency_code: string; currency_code: string;
logo: string; logo: string;
} }
@ -77,7 +77,7 @@ export interface ICreateAccountResponseDTO {
default_tax: number; default_tax: number;
status: string; status: string;
lang_code: string; language_code: string;
currency_code: string; currency_code: string;
logo: string; logo: string;
} }
@ -108,7 +108,7 @@ export interface IUpdateAccountResponseDTO {
default_tax: number; default_tax: number;
status: string; status: string;
lang_code: string; language_code: string;
currency_code: string; currency_code: string;
logo: string; logo: string;
} }

View File

@ -27,7 +27,7 @@ export const ICreateAccountRequestSchema = z.object({
default_tax: z.number(), default_tax: z.number(),
status: z.string(), status: z.string(),
lang_code: z.string(), language_code: z.string(),
currency_code: z.string(), currency_code: z.string(),
logo: z.string(), logo: z.string(),
}); });
@ -55,7 +55,7 @@ export const IUpdateAccountRequestSchema = z.object({
default_tax: z.number(), default_tax: z.number(),
status: z.string(), status: z.string(),
lang_code: z.string(), language_code: z.string(),
currency_code: z.string(), currency_code: z.string(),
logo: z.string(), logo: z.string(),
}); });

View File

@ -63,7 +63,7 @@ export class ContactMapper
legalRecord: source.legal_record, legalRecord: source.legal_record,
defaultTax: source.default_tax, defaultTax: source.default_tax,
status: source.status, status: source.status,
langCode: source.lang_code, langCode: source.language_code,
currencyCode: source.currency_code, currencyCode: source.currency_code,
}, },
idOrError.data idOrError.data
@ -96,7 +96,7 @@ export class ContactMapper
legal_record: source.legalRecord, legal_record: source.legalRecord,
default_tax: source.defaultTax, default_tax: source.defaultTax,
status: source.isActive ? "active" : "inactive", status: source.isActive ? "active" : "inactive",
lang_code: source.langCode, language_code: source.langCode,
currency_code: source.currencyCode, currency_code: source.currencyCode,
}); });
} }

View File

@ -41,7 +41,7 @@ export class ContactModel extends Model<
declare default_tax: number; declare default_tax: number;
declare status: string; declare status: string;
declare lang_code: string; declare language_code: string;
declare currency_code: string; declare currency_code: string;
} }
@ -130,7 +130,7 @@ export default (sequelize: Sequelize) => {
defaultValue: 2100, defaultValue: 2100,
}, },
lang_code: { language_code: {
type: DataTypes.STRING(2), type: DataTypes.STRING(2),
allowNull: false, allowNull: false,
defaultValue: "es", defaultValue: "es",

View File

@ -32,7 +32,7 @@ export const listContactsPresenter: IListContactsPresenter = {
default_tax: ensureNumber(contact.defaultTax), default_tax: ensureNumber(contact.defaultTax),
status: ensureString(contact.isActive ? "active" : "inactive"), status: ensureString(contact.isActive ? "active" : "inactive"),
lang_code: ensureString(contact.langCode), language_code: ensureString(contact.langCode),
currency_code: ensureString(contact.currencyCode), currency_code: ensureString(contact.currencyCode),
})), })),
}; };

View File

@ -22,6 +22,6 @@ export interface IListContactsResponseDTO {
default_tax: number; default_tax: number;
status: string; status: string;
lang_code: string; language_code: string;
currency_code: string; currency_code: string;
} }

View File

@ -20,7 +20,7 @@ i18n
detection: { detection: {
order: ["navigator"], order: ["navigator"],
}, },
debug: import.meta.env.DEV, debug: false, //import.meta.env.DEV,
fallbackLng: "es", fallbackLng: "es",
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,

View File

@ -31,8 +31,6 @@ export const getAppRouter = () => {
const grouped = groupModulesByLayout(modules); const grouped = groupModulesByLayout(modules);
console.debug(grouped);
return createBrowserRouter( return createBrowserRouter(
createRoutesFromElements( createRoutesFromElements(
<Route path='/'> <Route path='/'>

View File

@ -4,7 +4,7 @@ export interface IGetProfileResponseDTO {
id: string; id: string;
name: string; name: string;
email: string; email: string;
lang_code: string; language_code: string;
roles: string[]; roles: string[];
dealer: { dealer: {
id: string; id: string;
@ -16,7 +16,7 @@ export interface IGetProfileResponseDTO {
default_quote_validity: string; default_quote_validity: string;
default_tax: IPercentageDTO; default_tax: IPercentageDTO;
status: string; status: string;
lang_code: string; language_code: string;
currency_code: string; currency_code: string;
logo: string; logo: string;
}; };

View File

@ -7,7 +7,7 @@ export interface ILoginResponseDTO {
id: string; id: string;
name: string; name: string;
email: string; email: string;
lang_code: string; language_code: string;
roles: string[]; roles: string[];
token: string; token: string;
refresh_token: string; refresh_token: string;

View File

@ -1,6 +1,8 @@
export type DTO<T = unknown> = T; export type DTO<T = unknown> = T;
export type BinaryOutput = Buffer; // Puedes ampliar a Readable si usas streams export type BinaryOutput = Buffer; // Puedes ampliar a Readable si usas streams
export type IPresenterOutputParams = Record<string, unknown>;
export interface IPresenter<TSource = unknown, TOutput = DTO> { export interface IPresenter<TSource = unknown, TOutput = DTO> {
toOutput(source: TSource): TOutput | Promise<TOutput>; toOutput(source: TSource, params?: IPresenterOutputParams): TOutput | Promise<TOutput>;
} }

View File

@ -1,13 +1,9 @@
import { IPresenterRegistry } from "./presenter-registry.interface"; import { IPresenterRegistry } from "./presenter-registry.interface";
import { IPresenter } from "./presenter.interface"; import { IPresenter, IPresenterOutputParams } from "./presenter.interface";
export type IPresenterParams = {
presenterRegistry: IPresenterRegistry;
} & Record<string, unknown>;
export abstract class Presenter<TSource = unknown, TOutput = unknown> export abstract class Presenter<TSource = unknown, TOutput = unknown>
implements IPresenter<TSource, TOutput> implements IPresenter<TSource, TOutput>
{ {
constructor(protected presenterRegistry: IPresenterRegistry) {} constructor(protected presenterRegistry: IPresenterRegistry) {}
abstract toOutput(source: TSource): TOutput; abstract toOutput(source: TSource, params?: IPresenterOutputParams): TOutput;
} }

View File

@ -0,0 +1,11 @@
import { QuantityDTO } from "@erp/core";
import { Quantity } from "@repo/rdx-ddd";
export function formatQuantityDTO(quantity_value: QuantityDTO) {
const value = Quantity.create({
value: Number(quantity_value.value),
scale: Number(quantity_value.scale),
}).data;
return value.toNumber;
}

View File

@ -1,3 +1,4 @@
export * from "./format-money-dto"; export * from "./format-money-dto";
export * from "./format-percentage-dto"; export * from "./format-percentage-dto";
export * from "./format-quantity-dto";
export * from "./map-dto-to-customer-invoice-props"; export * from "./map-dto-to-customer-invoice-props";

View File

@ -1,12 +0,0 @@
import { MoneyDTO } from "@erp/core";
import { MoneyValue } from "@repo/rdx-ddd";
export function formatMoneyDTO(amount: MoneyDTO, locale: string) {
const money = MoneyValue.create({
value: Number(amount.value),
currency_code: amount.currency_code,
scale: Number(amount.scale),
}).data;
return money.format(locale);
}

View File

@ -9,7 +9,7 @@ type GetCustomerInvoiceItemByInvoiceIdResponseDTO = ArrayElement<
>; >;
export class CustomerInvoiceItemsFullPresenter extends Presenter { export class CustomerInvoiceItemsFullPresenter extends Presenter {
protected _map( private _mapItem(
invoiceItem: CustomerInvoiceItem, invoiceItem: CustomerInvoiceItem,
index: number index: number
): GetCustomerInvoiceItemByInvoiceIdResponseDTO { ): GetCustomerInvoiceItemByInvoiceIdResponseDTO {
@ -48,6 +48,6 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter {
} }
toOutput(invoiceItems: CustomerInvoiceItems): GetCustomerInvoiceByIdResponseDTO["items"] { toOutput(invoiceItems: CustomerInvoiceItems): GetCustomerInvoiceByIdResponseDTO["items"] {
return invoiceItems.map(this._map); return invoiceItems.map(this._mapItem);
} }
} }

View File

@ -0,0 +1,41 @@
import { IPresenterOutputParams, Presenter } from "@erp/core/api";
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import { ArrayElement } from "@repo/rdx-utils";
import { formatMoneyDTO, formatPercentageDTO, formatQuantityDTO } from "../../helpers";
type CustomerInvoiceItemsDTO = GetCustomerInvoiceByIdResponseDTO["items"];
type CustomerInvoiceItemDTO = ArrayElement<CustomerInvoiceItemsDTO>;
export class CustomerInvoiceItemsReportPersenter extends Presenter<
CustomerInvoiceItemsDTO,
unknown
> {
private _locale!: string;
private _mapItem(invoiceItem: CustomerInvoiceItemDTO, index: number) {
return {
...invoiceItem,
quantity: formatQuantityDTO(invoiceItem.quantity),
unit_amount: formatMoneyDTO(invoiceItem.unit_amount, this._locale),
subtotal_amount: formatMoneyDTO(invoiceItem.subtotal_amount, this._locale),
discount_percetage: formatPercentageDTO(invoiceItem.discount_percentage),
discount_amount: formatMoneyDTO(invoiceItem.discount_amount, this._locale),
taxes_amount: formatMoneyDTO(invoiceItem.taxes_amount, this._locale),
total_amount: formatMoneyDTO(invoiceItem.total_amount, this._locale),
};
}
toOutput(invoiceItems: CustomerInvoiceItemsDTO, params: IPresenterOutputParams): unknown {
const { locale } = params as {
locale: string;
};
this._locale = locale;
return invoiceItems.map((item, index) => {
return this._mapItem(item, index);
});
}
}

View File

@ -7,12 +7,22 @@ export class CustomerInvoiceReportPresenter extends Presenter<
unknown unknown
> { > {
toOutput(invoiceDTO: GetCustomerInvoiceByIdResponseDTO) { toOutput(invoiceDTO: GetCustomerInvoiceByIdResponseDTO) {
const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-items",
projection: "REPORT",
format: "JSON",
});
const locale = invoiceDTO.language_code; const locale = invoiceDTO.language_code;
const itemsDTO = itemsPresenter.toOutput(invoiceDTO.items, {
locale,
});
return { return {
...invoiceDTO, ...invoiceDTO,
items: itemsDTO,
subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, locale), subtotal_amount: formatMoneyDTO(invoiceDTO.subtotal_amount, locale),
percentage: formatPercentageDTO(invoiceDTO.discount_percentage), discount_percetage: formatPercentageDTO(invoiceDTO.discount_percentage),
discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, locale), discount_amount: formatMoneyDTO(invoiceDTO.discount_amount, locale),
taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, locale), taxes_amount: formatMoneyDTO(invoiceDTO.taxes_amount, locale),
total_amount: formatMoneyDTO(invoiceDTO.total_amount, locale), total_amount: formatMoneyDTO(invoiceDTO.total_amount, locale),

View File

@ -1,2 +1,3 @@
export * from "./customer-invoice-items.report.presenter";
export * from "./customer-invoice.report.presenter"; export * from "./customer-invoice.report.presenter";
export * from "./list-customer-invoices.presenter"; export * from "./list-customer-invoices.presenter";

View File

@ -23,6 +23,7 @@ import {
} from "../application"; } from "../application";
import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core"; import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core";
import { CustomerInvoiceItemsReportPersenter } from "../application/presenters/queries/customer-invoice-items.report.presenter";
import { CustomerInvoiceService } from "../domain"; import { CustomerInvoiceService } from "../domain";
import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers";
import { CustomerInvoiceRepository } from "./sequelize"; import { CustomerInvoiceRepository } from "./sequelize";
@ -108,6 +109,14 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
}, },
presenter: new CustomerInvoiceReportPresenter(presenterRegistry), presenter: new CustomerInvoiceReportPresenter(presenterRegistry),
}, },
{
key: {
resource: "customer-invoice-items",
projection: "REPORT",
format: "JSON",
},
presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry),
},
{ {
key: { key: {
resource: "customer-invoice", resource: "customer-invoice",

View File

@ -122,7 +122,7 @@
"placeholder": "Select default tax", "placeholder": "Select default tax",
"description": "The default tax rate for the customer" "description": "The default tax rate for the customer"
}, },
"lang_code": { "language_code": {
"label": "Language", "label": "Language",
"placeholder": "Select language", "placeholder": "Select language",
"description": "The preferred language of the customer" "description": "The preferred language of the customer"

View File

@ -124,7 +124,7 @@
"placeholder": "Seleccione el impuesto por defecto", "placeholder": "Seleccione el impuesto por defecto",
"description": "La tasa de impuesto por defecto para el cliente" "description": "La tasa de impuesto por defecto para el cliente"
}, },
"lang_code": { "language_code": {
"label": "Idioma", "label": "Idioma",
"placeholder": "Seleccione el idioma", "placeholder": "Seleccione el idioma",
"description": "El idioma preferido del cliente" "description": "El idioma preferido del cliente"

View File

@ -40,10 +40,10 @@ const defaultCustomerData = {
country: "ES", country: "ES",
postal_code: "28080", postal_code: "28080",
province: "Madrid", province: "Madrid",
lang_code: "es", language_code: "es",
currency_code: "EUR", currency_code: "EUR",
legal_record: "Registro Mercantil de Madrid, Tomo 12345, Folio 67, Hoja M-123456", legal_record: "Registro Mercantil de Madrid, Tomo 12345, Folio 67, Hoja M-123456",
default_tax: ["iva_21", "rec_5_2"], default_taxes: ["iva_21", "rec_5_2"],
}; };
interface CustomerFormProps { interface CustomerFormProps {
@ -290,20 +290,20 @@ export const CustomerEditForm = ({
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'> <CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'>
<TaxesMultiSelectField <TaxesMultiSelectField
control={form.control} control={form.control}
name='default_tax' name='default_taxes'
required required
label={t("form_fields.default_tax.label")} label={t("form_fields.default_taxes.label")}
placeholder={t("form_fields.default_tax.placeholder")} placeholder={t("form_fields.default_taxes.placeholder")}
description={t("form_fields.default_tax.description")} description={t("form_fields.default_taxes.description")}
/> />
<SelectField <SelectField
control={form.control} control={form.control}
name='lang_code' name='language_code'
required required
label={t("form_fields.lang_code.label")} label={t("form_fields.language_code.label")}
placeholder={t("form_fields.lang_code.placeholder")} placeholder={t("form_fields.language_code.placeholder")}
description={t("form_fields.lang_code.description")} description={t("form_fields.language_code.description")}
items={[ items={[
{ value: "es", label: "Español" }, { value: "es", label: "Español" },
{ value: "en", label: "Inglés" }, { value: "en", label: "Inglés" },

View File

@ -33,8 +33,8 @@ export function CustomerBasicInfoFields({ control }: { control: any }) {
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel> <FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
<FormControl> <FormControl>
<RadioGroup <RadioGroup
value={field.value ? "1" : "0"} onValueChange={field.onChange}
onValueChange={(val) => field.onChange(val === "1")} defaultValue={field.value ? "1" : "0"}
className='flex gap-6' className='flex gap-6'
> >
<FormItem className='flex items-center space-x-2'> <FormItem className='flex items-center space-x-2'>