Skip to content

Commit 62b4a80

Browse files
Diksha-28github-actions[bot]ijitendrasapMatejk00ijitendrasap
authored
feat: BOPIS feature with OPF CXSPA-10761 (#20674)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: ijitendrasap <jitendra.kumar.shah@sap> Co-authored-by: Mateusz Kolasa <[email protected]> Co-authored-by: ijitendrasap <[email protected]>
1 parent 83390ad commit 62b4a80

File tree

28 files changed

+541
-99
lines changed

28 files changed

+541
-99
lines changed

feature-libs/pickup-in-store/components/container/pickup-items-details/pickup-items-details.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { RouterModule } from '@angular/router';
1111
import {
1212
CmsConfig,
1313
ConfigModule,
14+
FeaturesConfigModule,
1415
I18nModule,
1516
UrlModule,
1617
} from '@spartacus/core';
@@ -28,6 +29,7 @@ import { PickUpItemsDetailsComponent } from './pickup-items-details.component';
2829
StoreModule,
2930
CardModule,
3031
MediaModule,
32+
FeaturesConfigModule,
3133
ConfigModule.withConfig({
3234
cmsComponents: {
3335
OrderConfirmationPickUpComponent: {

integration-libs/opf/checkout/assets/translations/en/opfCheckout.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"errors": {
3333
"loadActiveConfigurations": "We are unable to load payment options at this time. Please try again later.",
3434
"noActiveConfigurations": "There are no payment options available at this time. Please try again later or contact support.",
35-
"updateBillingAddress": "The address could not be updated. Please check that the address information is correct and that your device is connected to the internet. If the problem persists, you may need to clear your cart and start the checkout again."
35+
"updateBillingAddress": "The address could not be updated. Please check that the address information is correct and that your device is connected to the internet. If the problem persists, you may need to clear your cart and start the checkout again.",
36+
"noBillingAddress": "Billing address is required"
3637
}
3738
}
3839
}

integration-libs/opf/checkout/components/opf-checkout-billing-address-form/opf-checkout-billing-address-form.component.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ <h3>{{ 'opfCheckout.billingAddress' | cxTranslate }}</h3>
88
} as addressData"
99
>
1010
<div class="form-group">
11-
<div class="form-check">
11+
<div class="form-check" *ngIf="cart?.deliveryItemsQuantity">
1212
<label>
1313
<input
1414
type="checkbox"
@@ -50,9 +50,13 @@ <h3>{{ 'opfCheckout.billingAddress' | cxTranslate }}</h3>
5050
[showTitleCode]="true"
5151
[showCancelBtn]="true"
5252
actionBtnLabel="{{ 'common.save' | cxTranslate }}"
53-
cancelBtnLabel="{{ 'common.cancel' | cxTranslate }}"
53+
[cancelBtnLabel]="
54+
(service.paymentOptionsDisabled$ | async)
55+
? ('common.back' | cxTranslate)
56+
: ('common.cancel' | cxTranslate)
57+
"
5458
(submitAddress)="onSubmitAddress($event)"
55-
(backToAddress)="cancelAndHideForm()"
59+
(backToAddress)="onBackToAddress()"
5660
[setAsDefaultField]="false"
5761
[countries]="countries$"
5862
></cx-address-form>

integration-libs/opf/checkout/components/opf-checkout-billing-address-form/opf-checkout-billing-address-form.component.spec.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,25 @@
55

66
import { Pipe, PipeTransform } from '@angular/core';
77
import { ComponentFixture, TestBed } from '@angular/core/testing';
8-
import { Address, Country } from '@spartacus/core';
9-
import { BehaviorSubject, EMPTY, Observable, of } from 'rxjs';
8+
import {
9+
Address,
10+
Country,
11+
BaseSiteService,
12+
UserAddressAdapter,
13+
} from '@spartacus/core';
14+
import { BehaviorSubject, EMPTY, Observable, of, Subject } from 'rxjs';
1015
import { OpfCheckoutBillingAddressFormComponent } from './opf-checkout-billing-address-form.component';
1116
import { OpfCheckoutBillingAddressFormService } from './opf-checkout-billing-address-form.service';
17+
import { Store } from '@ngrx/store';
18+
import { ActiveCartFacade } from '@spartacus/cart/base/root';
19+
import { CheckoutStepService } from '@spartacus/checkout/base/components';
20+
import { ActivatedRoute } from '@angular/router';
1221

1322
class Service {
1423
billingAddress$ = new BehaviorSubject<Address | undefined>(undefined);
1524
isLoadingAddress$ = new BehaviorSubject<boolean>(false);
1625
isSameAsDelivery$ = new BehaviorSubject<boolean>(true);
26+
pickupNoDefaultAddress$ = new Subject<void>();
1727

1828
getCountries(): Observable<Country[]> {
1929
return EMPTY;
@@ -38,6 +48,7 @@ class Service {
3848
setIsSameAsDeliveryValue(value: boolean): void {
3949
this.isSameAsDelivery$.next(value);
4050
}
51+
setDefaultBillingAddress(): void {}
4152
}
4253

4354
@Pipe({
@@ -61,6 +72,17 @@ describe('OpfCheckoutBillingAddressFormComponent', () => {
6172
provide: OpfCheckoutBillingAddressFormService,
6273
useClass: Service,
6374
},
75+
{
76+
provide: ActiveCartFacade,
77+
useValue: {
78+
getActive: () => of({ code: '123', totalItems: 2 }),
79+
},
80+
},
81+
{ provide: Store, useValue: {} },
82+
{ provide: UserAddressAdapter, useValue: {} },
83+
{ provide: CheckoutStepService, useValue: {} },
84+
{ provide: BaseSiteService, useValue: {} },
85+
{ provide: ActivatedRoute, useValue: { params: of({}) } },
6486
],
6587
}).compileComponents();
6688

@@ -78,12 +100,14 @@ describe('OpfCheckoutBillingAddressFormComponent', () => {
78100
const countries = [{ id: '1', name: 'Country 1' }];
79101
spyOn(service, 'getCountries').and.returnValue(of(countries));
80102
spyOn(service, 'getAddresses');
103+
spyOn(service, 'setDefaultBillingAddress');
81104

82105
component.ngOnInit();
83106

84107
expect(component.countries$).toBeDefined();
85108
expect(service.getCountries).toHaveBeenCalled();
86109
expect(service.getAddresses).toHaveBeenCalled();
110+
expect(service.setDefaultBillingAddress).toHaveBeenCalled();
87111
});
88112

89113
it('should cancel and hide form on cancelAndHideForm', () => {

integration-libs/opf/checkout/components/opf-checkout-billing-address-form/opf-checkout-billing-address-form.component.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,35 @@ import {
99
Component,
1010
OnInit,
1111
inject,
12+
OnDestroy,
1213
} from '@angular/core';
13-
import { Address, Country } from '@spartacus/core';
14+
import { Address, Country, UserAddressService } from '@spartacus/core';
1415
import { ICON_TYPE } from '@spartacus/storefront';
15-
import { Observable } from 'rxjs';
16+
import { Observable, Subscription } from 'rxjs';
1617
import { OpfCheckoutBillingAddressFormService } from './opf-checkout-billing-address-form.service';
18+
import { ActiveCartFacade, Cart } from '@spartacus/cart/base/root';
19+
import { ActivatedRoute } from '@angular/router';
20+
import { CheckoutStepService } from '@spartacus/checkout/base/components';
1721

1822
@Component({
1923
selector: 'cx-opf-checkout-billing-address-form',
2024
templateUrl: './opf-checkout-billing-address-form.component.html',
2125
changeDetection: ChangeDetectionStrategy.OnPush,
2226
standalone: false,
2327
})
24-
export class OpfCheckoutBillingAddressFormComponent implements OnInit {
28+
export class OpfCheckoutBillingAddressFormComponent
29+
implements OnInit, OnDestroy
30+
{
2531
protected service = inject(OpfCheckoutBillingAddressFormService);
32+
protected userAddressService = inject(UserAddressService);
33+
protected activeCartFacade = inject(ActiveCartFacade);
34+
protected checkoutStepService = inject(CheckoutStepService);
35+
protected activatedRoute = inject(ActivatedRoute);
36+
37+
protected cart: Cart | null = null;
2638

2739
iconTypes = ICON_TYPE;
40+
subscription = new Subscription();
2841

2942
billingAddress$ = this.service.billingAddress$;
3043
isLoadingAddress$ = this.service.isLoadingAddress$;
@@ -36,8 +49,19 @@ export class OpfCheckoutBillingAddressFormComponent implements OnInit {
3649
countries$: Observable<Country[]>;
3750

3851
ngOnInit() {
52+
this.subscription.add(
53+
this.activeCartFacade.getActive().subscribe((cart) => (this.cart = cart))
54+
);
3955
this.countries$ = this.service.getCountries();
56+
this.userAddressService.loadAddresses();
57+
this.service.setDefaultBillingAddress();
4058
this.service.getAddresses();
59+
this.subscription.add(
60+
this.service.pickupNoDefaultAddress$.subscribe(() => {
61+
this.isEditBillingAddress = true;
62+
this.isAddingBillingAddressInProgress = true;
63+
})
64+
);
4165
}
4266

4367
cancelAndHideForm(): void {
@@ -47,6 +71,17 @@ export class OpfCheckoutBillingAddressFormComponent implements OnInit {
4771
this.isAddingBillingAddressInProgress = false;
4872
}
4973
}
74+
back(): void {
75+
this.checkoutStepService.back(this.activatedRoute);
76+
}
77+
78+
onBackToAddress(): void {
79+
this.subscription.add(
80+
this.service.paymentOptionsDisabled$.subscribe((isDisabled) =>
81+
isDisabled ? this.back() : this.cancelAndHideForm()
82+
)
83+
);
84+
}
5085

5186
editCustomBillingAddress(): void {
5287
this.isEditBillingAddress = true;
@@ -78,6 +113,17 @@ export class OpfCheckoutBillingAddressFormComponent implements OnInit {
78113
return;
79114
}
80115

81-
this.service.setBillingAddress(address).subscribe();
116+
this.service.setBillingAddress(address).subscribe({
117+
next: () => {
118+
this.service.setPaymentOptionsDisabled(false);
119+
},
120+
error: () => {
121+
this.service.setPaymentOptionsDisabled(true);
122+
},
123+
});
124+
}
125+
126+
ngOnDestroy(): void {
127+
this.subscription.unsubscribe();
82128
}
83129
}

integration-libs/opf/checkout/components/opf-checkout-billing-address-form/opf-checkout-billing-address-form.service.spec.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ import {
1313
Address,
1414
GlobalMessageService,
1515
HttpErrorModel,
16+
UserAddressAdapter,
17+
UserAddressService,
1618
UserPaymentService,
1719
} from '@spartacus/core';
18-
import { of, throwError } from 'rxjs';
20+
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
1921
import { OpfCheckoutPaymentWrapperService } from '../opf-checkout-payment-wrapper';
2022
import { OpfCheckoutBillingAddressFormService } from './opf-checkout-billing-address-form.service';
23+
import { Store } from '@ngrx/store';
2124

2225
describe('OpfCheckoutBillingAddressFormService', () => {
2326
let service: OpfCheckoutBillingAddressFormService;
@@ -27,6 +30,8 @@ describe('OpfCheckoutBillingAddressFormService', () => {
2730
let mockActiveCartFacade: Partial<ActiveCartFacade>;
2831
let mockGlobalMessageService: Partial<GlobalMessageService>;
2932
let mockOpfCheckoutPaymentWrapperService: Partial<OpfCheckoutPaymentWrapperService>;
33+
let mockPickupNoDefaultAddress$: BehaviorSubject<void>;
34+
let mockUserAddressService: Partial<UserAddressService>;
3035

3136
const mockDeliveryAddress: Address = {
3237
id: '123',
@@ -54,6 +59,7 @@ describe('OpfCheckoutBillingAddressFormService', () => {
5459
reloadActiveCart: () => of(true),
5560
isStable: () => of(true),
5661
getActive: () => of({ sapBillingAddress: mockPaymentAddress } as Cart),
62+
hasDeliveryItems: () => of(false),
5763
};
5864

5965
mockGlobalMessageService = {
@@ -63,6 +69,11 @@ describe('OpfCheckoutBillingAddressFormService', () => {
6369
mockOpfCheckoutPaymentWrapperService = {
6470
reloadPaymentMode: () => {},
6571
};
72+
mockPickupNoDefaultAddress$ = new BehaviorSubject<void>(undefined);
73+
74+
mockUserAddressService = {
75+
getDefaultAddress: () => of(undefined),
76+
};
6677

6778
TestBed.configureTestingModule({
6879
providers: [
@@ -82,6 +93,13 @@ describe('OpfCheckoutBillingAddressFormService', () => {
8293
provide: OpfCheckoutPaymentWrapperService,
8394
useValue: mockOpfCheckoutPaymentWrapperService,
8495
},
96+
{ provide: Store, useValue: { pipe: () => of(undefined) } },
97+
{ provide: UserAddressAdapter, useValue: {} },
98+
{
99+
provide: '_noDefaultAddressFoundForPickupMode$',
100+
useValue: mockPickupNoDefaultAddress$,
101+
},
102+
{ provide: UserAddressService, useValue: mockUserAddressService },
85103
],
86104
});
87105

@@ -251,4 +269,67 @@ describe('OpfCheckoutBillingAddressFormService', () => {
251269

252270
expect(service.setBillingAddress).not.toHaveBeenCalled();
253271
});
272+
273+
it('should return an observable from pickupNoDefaultAddress$', () => {
274+
spyOn(mockPickupNoDefaultAddress$, 'asObservable').and.callThrough();
275+
276+
(service as any)._noDefaultAddressFoundForPickupMode$ =
277+
mockPickupNoDefaultAddress$;
278+
279+
const result: Observable<void> = service.pickupNoDefaultAddress$;
280+
281+
expect(mockPickupNoDefaultAddress$.asObservable).toHaveBeenCalled();
282+
expect(result).toEqual(mockPickupNoDefaultAddress$.asObservable());
283+
});
284+
285+
it('should handle no default address by setting isSameAsDelivery=false and emitting pickupNoDefaultAddress$', (done) => {
286+
spyOn(service, 'setIsSameAsDeliveryValue').and.callThrough();
287+
service.pickupNoDefaultAddress$.subscribe(() => {
288+
expect(service.setIsSameAsDeliveryValue).toHaveBeenCalledWith(false);
289+
done();
290+
});
291+
(service as any).handleNoDefaultAddress();
292+
});
293+
294+
it('should handle error when setting default billing address fails', fakeAsync(() => {
295+
spyOn(mockActiveCartFacade, 'hasDeliveryItems').and.returnValue(of(false));
296+
spyOn(mockUserAddressService, 'getDefaultAddress').and.returnValue(
297+
of(mockDeliveryAddress)
298+
);
299+
spyOn(service, 'setBillingAddress').and.returnValue(
300+
throwError(() => new Error('Error'))
301+
);
302+
303+
service.setDefaultBillingAddress();
304+
flush();
305+
306+
expect(service['_$isLoadingAddress'].value).toBeFalsy();
307+
}));
308+
309+
it('should handle the absence of a default address by invoking handleNoDefaultAddress', fakeAsync(() => {
310+
spyOn(mockActiveCartFacade, 'hasDeliveryItems').and.returnValue(of(false));
311+
spyOn(mockUserAddressService, 'getDefaultAddress').and.returnValue(
312+
of(undefined)
313+
);
314+
const handleNoDefaultAddressSpy = spyOn(
315+
service as any,
316+
'handleNoDefaultAddress'
317+
).and.callThrough();
318+
319+
service.setDefaultBillingAddress();
320+
flush();
321+
322+
expect(handleNoDefaultAddressSpy).toHaveBeenCalled();
323+
}));
324+
it('should handle errors when loading the default address', fakeAsync(() => {
325+
spyOn(mockActiveCartFacade, 'hasDeliveryItems').and.returnValue(of(false));
326+
spyOn(mockUserAddressService, 'getDefaultAddress').and.returnValue(
327+
throwError(() => new Error('Error loading default address'))
328+
);
329+
330+
service.setDefaultBillingAddress();
331+
flush();
332+
333+
expect(service['_$isLoadingAddress'].value).toBeFalsy();
334+
}));
254335
});

0 commit comments

Comments
 (0)