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()"
[discount]="cartDiscount()"
[total]="cartTotal()"
[readonly]="!cartEditingEnabled()"
[readonly]="!canModifyCart()"
[allowModify]="canModifyCart()"
[allowUpdateQuantity]="canUpdateCartQuantity()"
[allowUpdateVariant]="canUpdateCartVariant()"
[allowDelete]="canDeleteCartItems()"
[backgroundColor]="'#ffffff'"
(closed)="isCartOpen.set(false)"
>

View File

@@ -32,6 +32,13 @@ const tenant: Tenant = {
footer_logo: 'https://example.com/footer.png',
header_bg_image: 'https://example.com/header-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: [],
menues: [
{
@@ -647,7 +654,16 @@ describe('StoreLayoutComponent', () => {
});
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({
id: 1,
tenant_codigo: tenant.codigo,
@@ -676,4 +692,62 @@ describe('StoreLayoutComponent', () => {
expect(cartItem.componentInstance.readonly()).toBe(true);
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 isCreatingPurchase = signal(false);
protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true);
protected readonly cartEditingEnabled = computed(
() => this.tenant()?.cart_editing_enabled ?? true,
protected readonly cartEditingPolicy = computed(() => this.tenant()?.cart_editing_policy ?? null);
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(() => {
@@ -92,6 +102,7 @@ export class StoreLayoutComponent implements OnInit {
attributes,
quantity: item.cantidad,
variantId: item.variant_id,
variants: item.variants,
};
}

View File

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

View File

@@ -1,5 +1,15 @@
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 {
id: number;
tenant_code: string;
@@ -127,7 +137,7 @@ export interface Tenant {
display_categories?: boolean;
display_seach_bar?: boolean;
display_cart?: boolean;
cart_editing_enabled?: boolean;
cart_editing_policy?: CartEditingPolicy;
display_cart_item_images?: boolean;
social_media?: SocialMedia[];
menues?: Menu[];

View File

@@ -58,11 +58,13 @@
[subtotal]="cartSubtotal()"
[discount]="cartDiscount()"
[total]="cartTotal()"
[readonly]="!cartEditingEnabled()"
[allowEditing]="
[readonly]="!canModifyCart()"
[allowModify]="canModifyCart()"
[allowUpdateQuantity]="canUpdateCartQuantity()"
[requireEditingMode]="
createdPurchase()?.status === 'created' || createdPurchase()?.status === 'pending_payment'
"
[allowRemove]="false"
[allowDelete]="false"
[persistQuantityChanges]="false"
[editing]="isEditingItems()"
[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 { CatalogService } from '../../../../core/services/catalog/catalog.service';
import { TenantService } from '../../../../core/services/tenant.service';
import { CartEditingPolicy } from '../../../../core/services/tenant.interface';
import { CheckoutPageComponent } from './checkout-page.component';
describe('CheckoutPageComponent payment validation', () => {
@@ -32,7 +33,12 @@ describe('CheckoutPageComponent payment validation', () => {
let routerStub: { navigate: ReturnType<typeof vi.fn> };
let routeQueryParamMap: ReturnType<typeof convertToParamMap>;
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(() => {
try {
@@ -87,7 +93,16 @@ describe('CheckoutPageComponent payment validation', () => {
routerStub = { navigate: vi.fn() };
routeQueryParamMap = convertToParamMap({});
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({
imports: [CheckoutPageComponent],
@@ -467,12 +482,22 @@ describe('CheckoutPageComponent payment validation', () => {
});
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();
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(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 isLoadingPurchase = signal(true);
protected readonly checkoutStepIndex = signal(0);
protected readonly cartEditingEnabled = computed(
() => this.tenantService.tenant()?.cart_editing_enabled ?? true,
protected readonly canUpdateCartQuantity = computed(
() => 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(() => {
@@ -181,7 +184,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
return;
}
if (editing && !this.cartEditingEnabled()) {
if (editing && !this.canModifyCart()) {
return;
}
@@ -231,7 +234,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
const itemId = event.item.cartItemId;
if (
!this.cartEditingEnabled() ||
!this.canUpdateCartQuantity() ||
!tenant ||
!purchaseId ||
!itemId ||

View File

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

View File

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

View File

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

View File

@@ -340,7 +340,7 @@ describe('CartComponent', () => {
quantity: 1,
},
]);
fixture.componentRef.setInput('allowEditing', true);
fixture.componentRef.setInput('requireEditingMode', true);
const editingChange = vi.fn();
fixture.componentInstance.editing.subscribe(editingChange);
fixture.detectChanges();
@@ -408,7 +408,7 @@ describe('CartComponent', () => {
).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({
imports: [CartComponent],
providers: [
@@ -441,8 +441,9 @@ describe('CartComponent', () => {
quantity: 1,
},
]);
fixture.componentRef.setInput('allowEditing', true);
fixture.componentRef.setInput('editable', false);
fixture.componentRef.setInput('requireEditingMode', true);
fixture.componentRef.setInput('allowModify', false);
fixture.componentRef.setInput('allowUpdateQuantity', false);
const quantityChange = vi.fn();
fixture.componentInstance.itemQuantityChange.subscribe(quantityChange);
fixture.detectChanges();

View File

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