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