Merge branch 'homologacion'
This commit is contained in:
@@ -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)"
|
||||
>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface CartItem {
|
||||
nombre: string | null;
|
||||
imagen: string | null;
|
||||
variant: CartItemVariant | null;
|
||||
variants?: CartItemVariant[];
|
||||
}
|
||||
|
||||
export interface Cart {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user