Merge branch 'homologacion'

This commit is contained in:
2026-08-19 15:36:19 -03:00
13 changed files with 185 additions and 32 deletions

View File

@@ -29,7 +29,11 @@
[subtotal]="cartSubtotal()" [subtotal]="cartSubtotal()"
[discount]="cartDiscount()" [discount]="cartDiscount()"
[total]="cartTotal()" [total]="cartTotal()"
[readonly]="!cartEditingEnabled()" [readonly]="!canModifyCart()"
[allowModify]="canModifyCart()"
[allowUpdateQuantity]="canUpdateCartQuantity()"
[allowUpdateVariant]="canUpdateCartVariant()"
[allowDelete]="canDeleteCartItems()"
[backgroundColor]="'#ffffff'" [backgroundColor]="'#ffffff'"
(closed)="isCartOpen.set(false)" (closed)="isCartOpen.set(false)"
> >

View File

@@ -32,6 +32,13 @@ const tenant: Tenant = {
footer_logo: 'https://example.com/footer.png', footer_logo: 'https://example.com/footer.png',
header_bg_image: 'https://example.com/header-background.png', header_bg_image: 'https://example.com/header-background.png',
footer_bg_image: 'https://example.com/footer-background.png', footer_bg_image: 'https://example.com/footer-background.png',
cart_editing_policy: {
code: 'full',
allow_modify: true,
allow_delete: true,
allow_update_quantity: true,
allow_update_variant: true,
},
categories: [], categories: [],
menues: [ menues: [
{ {
@@ -647,7 +654,16 @@ describe('StoreLayoutComponent', () => {
}); });
it('hides quantity selectors when the tenant disables cart editing', () => { it('hides quantity selectors when the tenant disables cart editing', () => {
tenantState.set({ ...tenant, cart_editing_enabled: false }); tenantState.set({
...tenant,
cart_editing_policy: {
code: 'disabled',
allow_modify: false,
allow_delete: false,
allow_update_quantity: false,
allow_update_variant: false,
},
});
cartState.set({ cartState.set({
id: 1, id: 1,
tenant_codigo: tenant.codigo, tenant_codigo: tenant.codigo,
@@ -676,4 +692,62 @@ describe('StoreLayoutComponent', () => {
expect(cartItem.componentInstance.readonly()).toBe(true); expect(cartItem.componentInstance.readonly()).toBe(true);
expect(cartItem.nativeElement.querySelector('app-quantity-selector')).toBeNull(); expect(cartItem.nativeElement.querySelector('app-quantity-selector')).toBeNull();
}); });
it('shows variant selectors only for the full cart editing policy', () => {
const variantCart: Cart = {
id: 1,
tenant_codigo: tenant.codigo,
status: 'active',
subtotal: '100.00',
items: [
{
id: 1,
cantidad: 1,
precio_unitario: '100.00',
catalog_item_id: 1,
variant_id: 10,
nombre: 'Producto',
imagen: null,
variant: { id: 10, precio: '100.00', stock_tecnico: 5, values: { talle: 'M' } },
variants: [
{ id: 10, precio: '100.00', stock_tecnico: 5, values: { talle: 'M' } },
{ id: 11, precio: '100.00', stock_tecnico: 5, values: { talle: 'L' } },
],
},
],
};
tenantState.set({
...tenant,
cart_editing_policy: {
code: 'quantity_and_remove',
allow_modify: true,
allow_delete: true,
allow_update_quantity: true,
allow_update_variant: false,
},
});
cartState.set(variantCart);
const fixture = TestBed.createComponent(StoreLayoutComponent);
fixture.detectChanges();
(fixture.componentInstance as any).isCartOpen.set(true);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('app-variant-selector')).toBeNull();
expect(fixture.nativeElement.querySelector('app-quantity-selector')).not.toBeNull();
tenantState.set({
...tenant,
cart_editing_policy: {
code: 'full',
allow_modify: true,
allow_delete: true,
allow_update_quantity: true,
allow_update_variant: true,
},
});
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('app-variant-selector')).not.toBeNull();
});
}); });

View File

@@ -36,8 +36,18 @@ export class StoreLayoutComponent implements OnInit {
protected readonly isCartOpen = signal(false); protected readonly isCartOpen = signal(false);
protected readonly isCreatingPurchase = signal(false); protected readonly isCreatingPurchase = signal(false);
protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true); protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true);
protected readonly cartEditingEnabled = computed( protected readonly cartEditingPolicy = computed(() => this.tenant()?.cart_editing_policy ?? null);
() => this.tenant()?.cart_editing_enabled ?? true, protected readonly canModifyCart = computed(
() => this.cartEditingPolicy()?.allow_modify ?? false,
);
protected readonly canDeleteCartItems = computed(
() => this.cartEditingPolicy()?.allow_delete ?? false,
);
protected readonly canUpdateCartQuantity = computed(
() => this.cartEditingPolicy()?.allow_update_quantity ?? false,
);
protected readonly canUpdateCartVariant = computed(
() => this.cartEditingPolicy()?.allow_update_variant ?? false,
); );
protected readonly cartSubtotal = computed(() => { protected readonly cartSubtotal = computed(() => {
@@ -92,6 +102,7 @@ export class StoreLayoutComponent implements OnInit {
attributes, attributes,
quantity: item.cantidad, quantity: item.cantidad,
variantId: item.variant_id, variantId: item.variant_id,
variants: item.variants,
}; };
} }

View File

@@ -24,6 +24,7 @@ export interface CartItem {
nombre: string | null; nombre: string | null;
imagen: string | null; imagen: string | null;
variant: CartItemVariant | null; variant: CartItemVariant | null;
variants?: CartItemVariant[];
} }
export interface Cart { export interface Cart {

View File

@@ -1,5 +1,15 @@
import { ApiResponse } from './api-response.interface'; import { ApiResponse } from './api-response.interface';
export type CartEditingPolicyCode = 'disabled' | 'quantity_and_remove' | 'full';
export interface CartEditingPolicy {
code: CartEditingPolicyCode;
allow_modify: boolean;
allow_delete: boolean;
allow_update_quantity: boolean;
allow_update_variant: boolean;
}
export interface BankAccount { export interface BankAccount {
id: number; id: number;
tenant_code: string; tenant_code: string;
@@ -127,7 +137,7 @@ export interface Tenant {
display_categories?: boolean; display_categories?: boolean;
display_seach_bar?: boolean; display_seach_bar?: boolean;
display_cart?: boolean; display_cart?: boolean;
cart_editing_enabled?: boolean; cart_editing_policy?: CartEditingPolicy;
display_cart_item_images?: boolean; display_cart_item_images?: boolean;
social_media?: SocialMedia[]; social_media?: SocialMedia[];
menues?: Menu[]; menues?: Menu[];

View File

@@ -58,11 +58,13 @@
[subtotal]="cartSubtotal()" [subtotal]="cartSubtotal()"
[discount]="cartDiscount()" [discount]="cartDiscount()"
[total]="cartTotal()" [total]="cartTotal()"
[readonly]="!cartEditingEnabled()" [readonly]="!canModifyCart()"
[allowEditing]=" [allowModify]="canModifyCart()"
[allowUpdateQuantity]="canUpdateCartQuantity()"
[requireEditingMode]="
createdPurchase()?.status === 'created' || createdPurchase()?.status === 'pending_payment' createdPurchase()?.status === 'created' || createdPurchase()?.status === 'pending_payment'
" "
[allowRemove]="false" [allowDelete]="false"
[persistQuantityChanges]="false" [persistQuantityChanges]="false"
[editing]="isEditingItems()" [editing]="isEditingItems()"
[editingDisabled]="isUpdatingItem() || isPreparingItemEdit()" [editingDisabled]="isUpdatingItem() || isPreparingItemEdit()"

View File

@@ -10,6 +10,7 @@ import { CartService } from '../../../../core/services/cart/cart.service';
import { CheckoutService } from '../../../../core/services/checkout.service'; import { CheckoutService } from '../../../../core/services/checkout.service';
import { CatalogService } from '../../../../core/services/catalog/catalog.service'; import { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { TenantService } from '../../../../core/services/tenant.service'; import { TenantService } from '../../../../core/services/tenant.service';
import { CartEditingPolicy } from '../../../../core/services/tenant.interface';
import { CheckoutPageComponent } from './checkout-page.component'; import { CheckoutPageComponent } from './checkout-page.component';
describe('CheckoutPageComponent payment validation', () => { describe('CheckoutPageComponent payment validation', () => {
@@ -32,7 +33,12 @@ describe('CheckoutPageComponent payment validation', () => {
let routerStub: { navigate: ReturnType<typeof vi.fn> }; let routerStub: { navigate: ReturnType<typeof vi.fn> };
let routeQueryParamMap: ReturnType<typeof convertToParamMap>; let routeQueryParamMap: ReturnType<typeof convertToParamMap>;
let authUserState: ReturnType<typeof signal>; let authUserState: ReturnType<typeof signal>;
let tenantState: ReturnType<typeof signal<{ codigo: string; cart_editing_enabled?: boolean }>>; let tenantState: ReturnType<
typeof signal<{
codigo: string;
cart_editing_policy?: CartEditingPolicy;
}>
>;
beforeAll(() => { beforeAll(() => {
try { try {
@@ -87,7 +93,16 @@ describe('CheckoutPageComponent payment validation', () => {
routerStub = { navigate: vi.fn() }; routerStub = { navigate: vi.fn() };
routeQueryParamMap = convertToParamMap({}); routeQueryParamMap = convertToParamMap({});
authUserState = signal(null); authUserState = signal(null);
tenantState = signal({ codigo: 'tenant-test' }); tenantState = signal({
codigo: 'tenant-test',
cart_editing_policy: {
code: 'full',
allow_modify: true,
allow_delete: true,
allow_update_quantity: true,
allow_update_variant: true,
},
});
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CheckoutPageComponent], imports: [CheckoutPageComponent],
@@ -467,12 +482,22 @@ describe('CheckoutPageComponent payment validation', () => {
}); });
it('does not allow item editing when the tenant disables cart editing', async () => { it('does not allow item editing when the tenant disables cart editing', async () => {
tenantState.set({ codigo: 'tenant-test', cart_editing_enabled: false }); tenantState.set({
codigo: 'tenant-test',
cart_editing_policy: {
code: 'disabled',
allow_modify: false,
allow_delete: false,
allow_update_quantity: false,
allow_update_variant: false,
},
});
const { component } = createComponent(); const { component } = createComponent();
await component.onEditingItemsChange(true); await component.onEditingItemsChange(true);
expect(component.cartEditingEnabled()).toBe(false); expect(component.canUpdateCartQuantity()).toBe(false);
expect(component.canModifyCart()).toBe(false);
expect(component.isEditingItems()).toBe(false); expect(component.isEditingItems()).toBe(false);
expect(checkoutServiceStub.prepareItemEditing).not.toHaveBeenCalled(); expect(checkoutServiceStub.prepareItemEditing).not.toHaveBeenCalled();
}); });

View File

@@ -79,8 +79,11 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
protected readonly createdPurchase = signal<PurchaseDetailResponse | null>(null); protected readonly createdPurchase = signal<PurchaseDetailResponse | null>(null);
protected readonly isLoadingPurchase = signal(true); protected readonly isLoadingPurchase = signal(true);
protected readonly checkoutStepIndex = signal(0); protected readonly checkoutStepIndex = signal(0);
protected readonly cartEditingEnabled = computed( protected readonly canUpdateCartQuantity = computed(
() => this.tenantService.tenant()?.cart_editing_enabled ?? true, () => this.tenantService.tenant()?.cart_editing_policy?.allow_update_quantity ?? false,
);
protected readonly canModifyCart = computed(
() => this.tenantService.tenant()?.cart_editing_policy?.allow_modify ?? false,
); );
protected readonly cartSubtotal = computed(() => { protected readonly cartSubtotal = computed(() => {
@@ -181,7 +184,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
return; return;
} }
if (editing && !this.cartEditingEnabled()) { if (editing && !this.canModifyCart()) {
return; return;
} }
@@ -231,7 +234,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
const itemId = event.item.cartItemId; const itemId = event.item.cartItemId;
if ( if (
!this.cartEditingEnabled() || !this.canUpdateCartQuantity() ||
!tenant || !tenant ||
!purchaseId || !purchaseId ||
!itemId || !itemId ||

View File

@@ -26,7 +26,7 @@
</div> </div>
</div> </div>
@if (hasVariantSelectors() && !quantityDisabled()) { @if (hasVariantSelectors() && !variantDisabled()) {
<app-variant-selector <app-variant-selector
class="cart-item-variant-selector" class="cart-item-variant-selector"
[variants]="variants()" [variants]="variants()"
@@ -55,7 +55,7 @@
(increase)="onIncrease()" (increase)="onIncrease()"
(decrease)="onDecrease()" (decrease)="onDecrease()"
/> />
@if (!quantityDisabled() && showRemove()) { @if (!removeDisabled() && allowDelete()) {
<app-icon-button variant="trash" class="cart-item-remove-btn" (click)="onRemove()" /> <app-icon-button variant="trash" class="cart-item-remove-btn" (click)="onRemove()" />
} }
</div> </div>

View File

@@ -31,7 +31,9 @@ export class CartItemComponent {
readonly quantity = input<number>(1); readonly quantity = input<number>(1);
readonly readonly = input<boolean>(false); readonly readonly = input<boolean>(false);
readonly quantityDisabled = input<boolean>(false); readonly quantityDisabled = input<boolean>(false);
readonly showRemove = input<boolean>(true); readonly variantDisabled = input<boolean>(false);
readonly removeDisabled = input<boolean>(false);
readonly allowDelete = input<boolean>(true);
readonly quantityChange = output<number>(); readonly quantityChange = output<number>();
readonly remove = output<void>(); readonly remove = output<void>();
@@ -61,7 +63,7 @@ export class CartItemComponent {
} }
protected onVariantChange(variant: unknown): void { protected onVariantChange(variant: unknown): void {
if (!this.quantityDisabled() && typeof variant === 'number') { if (!this.variantDisabled() && typeof variant === 'number') {
this.variantChange.emit(variant); this.variantChange.emit(variant);
} }
} }

View File

@@ -6,7 +6,7 @@
<h2 class="m-0 text-uppercase fw-bold cart-title">{{ title() }}</h2> <h2 class="m-0 text-uppercase fw-bold cart-title">{{ title() }}</h2>
<div class="d-flex align-items-center cart-header-actions"> <div class="d-flex align-items-center cart-header-actions">
@if (!readonly() && editable() && allowEditing() && items().length > 0) { @if (!readonly() && allowModify() && requireEditingMode() && items().length > 0) {
<button <button
class="btn btn-link p-0 border-0 cart-edit-btn" class="btn btn-link p-0 border-0 cart-edit-btn"
type="button" type="button"
@@ -48,8 +48,22 @@
[selectedVariant]="getItemVariant(item)" [selectedVariant]="getItemVariant(item)"
[quantity]="getItemQuantity(item)" [quantity]="getItemQuantity(item)"
[readonly]="readonly()" [readonly]="readonly()"
[quantityDisabled]="!editable() || editingDisabled() || (allowEditing() && !editing())" [quantityDisabled]="
[showRemove]="allowRemove()" readonly() ||
!allowUpdateQuantity() ||
editingDisabled() ||
(requireEditingMode() && !editing())
"
[variantDisabled]="
readonly() ||
!allowUpdateVariant() ||
editingDisabled() ||
(requireEditingMode() && !editing())
"
[removeDisabled]="
readonly() || !allowDelete() || editingDisabled() || (requireEditingMode() && !editing())
"
[allowDelete]="allowDelete()"
(quantityChange)="onItemQuantityChange(idx, $event)" (quantityChange)="onItemQuantityChange(idx, $event)"
(variantChange)="onItemVariantChange(idx, $event)" (variantChange)="onItemVariantChange(idx, $event)"
(remove)="onItemRemove(idx)" (remove)="onItemRemove(idx)"

View File

@@ -340,7 +340,7 @@ describe('CartComponent', () => {
quantity: 1, quantity: 1,
}, },
]); ]);
fixture.componentRef.setInput('allowEditing', true); fixture.componentRef.setInput('requireEditingMode', true);
const editingChange = vi.fn(); const editingChange = vi.fn();
fixture.componentInstance.editing.subscribe(editingChange); fixture.componentInstance.editing.subscribe(editingChange);
fixture.detectChanges(); fixture.detectChanges();
@@ -408,7 +408,7 @@ describe('CartComponent', () => {
).toBe(false); ).toBe(false);
}); });
it('hides the edit toggle and disables quantity changes when editable is false', async () => { it('hides the edit toggle and disables quantity changes when quantity updates are false', async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CartComponent], imports: [CartComponent],
providers: [ providers: [
@@ -441,8 +441,9 @@ describe('CartComponent', () => {
quantity: 1, quantity: 1,
}, },
]); ]);
fixture.componentRef.setInput('allowEditing', true); fixture.componentRef.setInput('requireEditingMode', true);
fixture.componentRef.setInput('editable', false); fixture.componentRef.setInput('allowModify', false);
fixture.componentRef.setInput('allowUpdateQuantity', false);
const quantityChange = vi.fn(); const quantityChange = vi.fn();
fixture.componentInstance.itemQuantityChange.subscribe(quantityChange); fixture.componentInstance.itemQuantityChange.subscribe(quantityChange);
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -53,9 +53,11 @@ export class CartComponent {
readonly total = input<number>(0); readonly total = input<number>(0);
readonly backgroundColor = input<string>('#ffffff'); readonly backgroundColor = input<string>('#ffffff');
readonly readonly = input<boolean>(false); readonly readonly = input<boolean>(false);
readonly editable = input<boolean>(true); readonly allowUpdateQuantity = input<boolean>(true);
readonly allowEditing = input<boolean>(false); readonly allowModify = input<boolean>(true);
readonly allowRemove = input<boolean>(true); readonly requireEditingMode = input<boolean>(false);
readonly allowUpdateVariant = input<boolean>(true);
readonly allowDelete = input<boolean>(true);
readonly persistQuantityChanges = input<boolean>(true); readonly persistQuantityChanges = input<boolean>(true);
readonly editingDisabled = input<boolean>(false); readonly editingDisabled = input<boolean>(false);
readonly editing = model<boolean>(false); readonly editing = model<boolean>(false);
@@ -125,7 +127,7 @@ export class CartComponent {
} }
protected onItemQuantityChange(index: number, newQuantity: number): void { protected onItemQuantityChange(index: number, newQuantity: number): void {
if (!this.editable()) { if (!this.allowUpdateQuantity()) {
return; return;
} }
@@ -170,6 +172,8 @@ export class CartComponent {
} }
protected onItemVariantChange(index: number, variantId: number): void { protected onItemVariantChange(index: number, variantId: number): void {
if (this.readonly() || !this.allowUpdateVariant() || this.editingDisabled()) return;
const item = this.items()[index]; const item = this.items()[index];
const cartItemId = item?.cartItemId; const cartItemId = item?.cartItemId;
@@ -200,6 +204,8 @@ export class CartComponent {
} }
protected onItemRemove(index: number): void { protected onItemRemove(index: number): void {
if (this.readonly() || !this.allowDelete() || this.editingDisabled()) return;
const target = this.resolveRemoveTarget(index); const target = this.resolveRemoveTarget(index);
if (!target) { if (!target) {
@@ -225,7 +231,7 @@ export class CartComponent {
protected readonly formattedTotal = computed(() => this.formatCurrency(this.total())); protected readonly formattedTotal = computed(() => this.formatCurrency(this.total()));
protected toggleEditing(): void { protected toggleEditing(): void {
if (!this.editable() || this.editingDisabled()) { if (!this.allowModify() || this.editingDisabled()) {
return; return;
} }