From 67874500193eb6113e1bca8028bccaf966976996 Mon Sep 17 00:00:00 2001 From: Cyril Joseph Date: Wed, 20 Aug 2025 14:31:13 -0300 Subject: [PATCH] carnet checkout updates --- src/app/app.routes.server.ts | 1 + src/app/app.routes.ts | 1 + .../carnet/checkout/checkout.component.html | 60 +++++++ .../carnet/checkout/checkout.component.scss | 36 ++++ src/app/carnet/checkout/checkout.component.ts | 162 ++++++++++++++++++ .../terms-conditions.component.ts | 62 +++++-- .../core/services/carnet/payment.service.ts | 29 ++++ src/index.html | 2 + src/styles.scss | 4 + 9 files changed, 341 insertions(+), 16 deletions(-) create mode 100644 src/app/carnet/checkout/checkout.component.html create mode 100644 src/app/carnet/checkout/checkout.component.scss create mode 100644 src/app/carnet/checkout/checkout.component.ts create mode 100644 src/app/core/services/carnet/payment.service.ts diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index 7e81881..483220b 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -15,6 +15,7 @@ export const serverRoutes: ServerRoute[] = [ { path: ':appId/add-holder', renderMode: RenderMode.Client }, { path: ':appId/edit-holder/:holderid', renderMode: RenderMode.Client }, { path: ':appId/terms', renderMode: RenderMode.Client }, + { path: ':appId/checkout', renderMode: RenderMode.Client }, { path: '**', renderMode: RenderMode.Prerender diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc46514..4b7304f 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -23,6 +23,7 @@ export const routes: Routes = [ { path: 'extend-carnet/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) }, { path: 'additional-sets/:headerid', loadComponent: () => import('./carnet/edit/edit-carnet.component').then(m => m.EditCarnetComponent) }, { path: 'terms', loadComponent: () => import('./carnet/terms-conditions/terms-conditions.component').then(m => m.TermsConditionsComponent) }, + { path: 'checkout', loadComponent: () => import('./carnet/checkout/checkout.component').then(m => m.CheckoutComponent) }, { path: '', redirectTo: 'home', pathMatch: 'full' } ], canActivate: [AuthGuard, AppIdGuard] diff --git a/src/app/carnet/checkout/checkout.component.html b/src/app/carnet/checkout/checkout.component.html new file mode 100644 index 0000000..a31dca8 --- /dev/null +++ b/src/app/carnet/checkout/checkout.component.html @@ -0,0 +1,60 @@ +
+ + + Checkout + + + + +
+
+ Basic fee: + {{estimatedFees.basicFee | currency}} +
+ +
+ Counterfoil fee: + {{estimatedFees.counterFoilFee | currency}} +
+ +
+ Continuation sheet fee: + {{estimatedFees.continuationSheetFee | currency}} +
+ +
+ Expedited fee: + {{estimatedFees.expeditedFee | currency}} +
+ +
+ Shipping fee: + {{estimatedFees.shippingFee | currency}} +
+ +
+ Bond Premium: + {{estimatedFees.bondPremium | currency}} +
+ +
+ Cargo Premium: + {{estimatedFees.cargoPremium | currency}} +
+ +
+ LDI Premium: + {{estimatedFees.ldiPremium | currency}} +
+
+ Total: + {{ orderTotal | currency }} +
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/carnet/checkout/checkout.component.scss b/src/app/carnet/checkout/checkout.component.scss new file mode 100644 index 0000000..bd89453 --- /dev/null +++ b/src/app/carnet/checkout/checkout.component.scss @@ -0,0 +1,36 @@ +.checkout-container { + max-width: 500px; + margin: 0 auto; + padding: 0 1rem; + + .mat-mdc-card-header { + justify-content: center; + padding: 1rem; + border-bottom: 1px solid #ddd; + margin-bottom: 0.5rem; + } + + mat-card-title { + color: var(--mat-sys-primary); + font-weight: 500; + } + + .order-summary { + .summary-item { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; + } + + .summary-item.total { + border-top: 1px solid #ddd; + padding-top: 0.5rem; + margin-top: 0.5rem; + font-size: 1.1rem; + } + } + + .paypal-container { + margin-top: 2rem; + } +} \ No newline at end of file diff --git a/src/app/carnet/checkout/checkout.component.ts b/src/app/carnet/checkout/checkout.component.ts new file mode 100644 index 0000000..7572591 --- /dev/null +++ b/src/app/carnet/checkout/checkout.component.ts @@ -0,0 +1,162 @@ +import { Component, ElementRef, inject, ViewChild } from '@angular/core'; +import { PaymentService } from '../../core/services/carnet/payment.service'; +import { CommonModule } from '@angular/common'; +import { Fees } from '../../core/models/carnet/fee'; +import { CarnetService } from '../../core/services/carnet/carnet.service'; +import { ApiErrorHandlerService } from '../../core/services/common/api-error-handler.service'; +import { NavigationService } from '../../core/services/common/navigation.service'; +import { NotificationService } from '../../core/services/common/notification.service'; +import { StorageService } from '../../core/services/common/storage.service'; +import { finalize } from 'rxjs'; +import { AngularMaterialModule } from '../../shared/module/angular-material.module'; + +// This is necessary to tell TypeScript that a global 'paypal' object exists. +declare var paypal: any; + +@Component({ + selector: 'app-checkout', + imports: [CommonModule, AngularMaterialModule], + templateUrl: './checkout.component.html', + styleUrl: './checkout.component.scss' +}) +export class CheckoutComponent { + paymentCompleted: boolean = false; + paymentError: boolean = false; + isLoading: boolean = false; + paymentDetails: any = null; + currentApplicationDetails: { headerid: number, applicationName: string, applicationType: string } | null = null; + changeInProgress: boolean = false; + estimatedFees: Fees = {}; + + @ViewChild('paypalcontrol', { static: true }) paypalElement!: ElementRef; + + orderDetails = { + price: '', + description: '', + }; + + orderTotal = 0.00; + currency = 'USD'; + + private paymentService = inject(PaymentService); + private notificationService = inject(NotificationService); + private errorHandler = inject(ApiErrorHandlerService); + private storageService = inject(StorageService); + private navigationService = inject(NavigationService); + private carnetService = inject(CarnetService); + + constructor() { + this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string, applicationType: string }>('currentapplication') + this.orderDetails.description = ` 'Carnet Application - ${this.currentApplicationDetails?.applicationName}` + } + + ngOnInit(): void { + if (this.currentApplicationDetails?.headerid) { + this.carnetService.getEstimatedFees(this.currentApplicationDetails?.headerid).subscribe({ + next: (data: Fees) => { + if (data) { + this.estimatedFees = data; + this.calculateTotal(); + } + }, + error: (error: any) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to get estimated fees'); + this.notificationService.showError(errorMessage); + console.error('Error getting estimated fees:', error); + } + }); + } + } + + ngAfterViewInit(): void { + this.initConfig(); + } + + initConfig(): void { + paypal + .Buttons({ + style: { + layout: 'vertical', // or 'horizontal' + color: 'gold', + shape: 'pill', + label: 'pay', + height: 35 // Height in pixels (default is ~35-45px) + + // 'paypal' – Standard PayPal button [defoult] + // 'checkout' – "Checkout with PayPal" + // 'buynow' – "Buy Now with PayPal" + // 'pay' – "Pay with PayPal" + // 'installment' – Displays installment information + // 'subscribe' – For subscriptions + // 'donate' – For donations + // pay : Pay with [PayPal] + // buynow : [PayPal] Buy Now + // Error: Expected style.height to be between 25px and 55px - got 10px + // width width usually doesn’t need to be set or must be reasonable. + }, + // 1. Call your server to set up the transaction + createOrder: async (data: any, actions: any) => { + try { + // Call the createOrder method from your service + const orderId = await this.paymentService.createOrder(this.orderDetails); + // console.log('Order ID created by server:', orderId); + return orderId; + } catch (error) { + this.notificationService.showError('Failed to initiate the payment'); + console.error('Error creating order:', error); + return; + } + }, + + // 2. Call your server to finalize the transaction + onApprove: async (data: any, actions: any) => { + try { + // data.orderID is the ID of the transaction from PayPal + const captureDetails = await this.paymentService.completePayment(data.orderID); + + // console.log('Capture details:', captureDetails); + + // You can now redirect the user or update the UI + this.changeInProgress = true; + this.carnetService.submitApplication(this.currentApplicationDetails?.headerid!).pipe(finalize(() => { + this.changeInProgress = false; + })).subscribe({ + next: () => { + this.notificationService.showSuccess('Application submitted successfully'); + this.storageService.removeItem('currentapplication'); + this.navigationService.navigate(["home"]); + }, + error: (error) => { + this.notificationService.showError('Failed to submit application'); + console.error('Error submitting the application', error); + } + }); + } catch (error) { + this.notificationService.showError('Failed to complete the payment'); + console.error('Error completing the payment:', error); + return; + } + }, + + // 3. Handle errors + onError: (error: any) => { + this.notificationService.showError('Payment transaction failed'); + console.error('An error occurred during the transaction:', error); + }, + + // 4. Handle cancellation + onCancel: (data: any) => { + } + }).render(this.paypalElement.nativeElement) + .catch((error: any) => { + console.error('Failed to render PayPal Buttons:', error); + }); + } + + calculateTotal(): void { + const validFees = Object.values(this.estimatedFees).filter( + (fee): fee is number => typeof fee === 'number' + ); + this.orderTotal = validFees.reduce((total, fee) => total + fee, 0); + } +} \ No newline at end of file diff --git a/src/app/carnet/terms-conditions/terms-conditions.component.ts b/src/app/carnet/terms-conditions/terms-conditions.component.ts index 22291d1..aa1e93d 100644 --- a/src/app/carnet/terms-conditions/terms-conditions.component.ts +++ b/src/app/carnet/terms-conditions/terms-conditions.component.ts @@ -13,6 +13,8 @@ import { StorageService } from '../../core/services/common/storage.service'; import { CarnetService } from '../../core/services/carnet/carnet.service'; import { Fees } from '../../core/models/carnet/fee'; import { finalize } from 'rxjs'; +import { ShippingService } from '../../core/services/carnet/shipping.service'; +import { Shipping } from '../../core/models/carnet/shipping'; @Component({ selector: 'app-terms-conditions', @@ -26,18 +28,20 @@ import { finalize } from 'rxjs'; styleUrl: './terms-conditions.component.scss' }) export class TermsConditionsComponent { - + isLoading: boolean = false; currentDate = new Date(); hasReadAllTerms = false; currentApplicationDetails: { headerid: number, applicationName: string, applicationType: string } | null = null; changeInProgress: boolean = false; estimatedFees: Fees = {}; + needCheckOut: boolean = false; private notificationService = inject(NotificationService); private errorHandler = inject(ApiErrorHandlerService); private storageService = inject(StorageService); private navigationService = inject(NavigationService); private carnetService = inject(CarnetService); + private shippingService = inject(ShippingService); constructor() { this.currentApplicationDetails = this.storageService.get<{ headerid: number, applicationName: string, applicationType: string }>('currentapplication') @@ -57,25 +61,31 @@ export class TermsConditionsComponent { console.error('Error getting estimated fees:', error); } }); + + this.getPaymentType(this.currentApplicationDetails?.headerid); } } onAccept(): void { - this.changeInProgress = true; - this.carnetService.submitApplication(this.currentApplicationDetails?.headerid!).pipe(finalize(() => { - this.changeInProgress = false; - })).subscribe({ - next: () => { - this.notificationService.showSuccess('Application submitted successfully'); - this.storageService.removeItem('currentapplication'); - this.navigationService.navigate(["home"]); - }, - error: (error) => { - let errorMessage = this.errorHandler.handleApiError(error, 'Failed to submit application'); - this.notificationService.showError(errorMessage); - console.error('Error submitting the application', error); - } - }); + if (this.needCheckOut) { + this.navigationService.navigate(['checkout']); + } else { + this.changeInProgress = true; + this.carnetService.submitApplication(this.currentApplicationDetails?.headerid!).pipe(finalize(() => { + this.changeInProgress = false; + })).subscribe({ + next: () => { + this.notificationService.showSuccess('Application submitted successfully'); + this.storageService.removeItem('currentapplication'); + this.navigationService.navigate(["home"]); + }, + error: (error) => { + let errorMessage = this.errorHandler.handleApiError(error, 'Failed to submit application'); + this.notificationService.showError(errorMessage); + console.error('Error submitting the application', error); + } + }); + } } onDecline(): void { @@ -100,4 +110,24 @@ export class TermsConditionsComponent { } }) } + + getPaymentType(headerid: number): void { + this.isLoading = true; + this.shippingService.getShippingData(headerid).pipe( + finalize(() => { + this.isLoading = false; + }) + ).subscribe({ + next: (shippingData: Shipping) => { + if (!shippingData) { // do nothing if empty + return; + } + this.needCheckOut = shippingData.paymentMethod !== 'ACH' && shippingData.paymentMethod !== 'INV'; + }, + error: (error: any) => { + console.error('Error loading shipping data', error); + this.errorHandler.handleApiError(error, 'Failed to load shipping data'); + } + }); + } } \ No newline at end of file diff --git a/src/app/core/services/carnet/payment.service.ts b/src/app/core/services/carnet/payment.service.ts new file mode 100644 index 0000000..1218014 --- /dev/null +++ b/src/app/core/services/carnet/payment.service.ts @@ -0,0 +1,29 @@ +import { inject, Injectable } from '@angular/core'; +import { environment } from '../../../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class PaymentService { + private apiUrl = environment.apiUrl; + private apiDb = environment.apiDb; + + private http = inject(HttpClient); + + createOrder(orderDetails: any): Promise { + return firstValueFrom( + this.http.post<{ id: string }>(`${this.apiUrl}/${this.apiDb}/InitiatePayment`, {}) + ).then(order => order.id); + } + + completePayment(orderId: string): Promise { + const data = { + P_ORDERID: orderId + } + + return firstValueFrom( + this.http.post(`${this.apiUrl}/oracle/CompletePayment`, data)); + } +} \ No newline at end of file diff --git a/src/index.html b/src/index.html index c3002e8..e714a84 100644 --- a/src/index.html +++ b/src/index.html @@ -9,6 +9,8 @@ + diff --git a/src/styles.scss b/src/styles.scss index 4291f69..062fbaa 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -111,4 +111,8 @@ body { /* Sort */ @include mat.sort-overrides((arrow-color: var(--mat-sys-on-primary), )); +} + +.paypal-buttons { + z-index: 5; } \ No newline at end of file