Skip to content

Commit

Permalink
Add initial 'Custom Offline' Provider/Proxy/Method.
Browse files Browse the repository at this point in the history
This adds a new 'CUSTOM_OFFLINE' provider & proxy with an 'ETRANSFER'
method as an example. The next step will be to allow dynamic methods
using this single new provider.
  • Loading branch information
shanebrowncs committed Jan 5, 2025
1 parent d7c50ad commit 4e21a69
Show file tree
Hide file tree
Showing 22 changed files with 377 additions and 10 deletions.
3 changes: 3 additions & 0 deletions frontend/projects/public/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {BookingComponent} from './reservation/booking/booking.component';
import {OverviewComponent} from './reservation/overview/overview.component';
import {SuccessComponent} from './reservation/success/success.component';
import {OfflinePaymentComponent} from './reservation/offline-payment/offline-payment.component';
import {CustomOfflinePaymentComponent} from './reservation/custom-offline-payment/custom-offline-payment.component';
import {ViewTicketComponent} from './view-ticket/view-ticket.component';
import {ReservationGuard} from './reservation/reservation.guard';
import {ProcessingPaymentComponent} from './reservation/processing-payment/processing-payment.component';
Expand Down Expand Up @@ -61,6 +62,7 @@ const routes: Routes = [
{ path: 'book', component: BookingComponent, canActivate: subscriptionReservationsGuard },
{ path: 'overview', component: OverviewComponent, canActivate: subscriptionReservationsGuard },
{ path: 'waiting-payment', component: OfflinePaymentComponent, canActivate: subscriptionReservationsGuard },
{ path: 'waiting-custom-payment', component: CustomOfflinePaymentComponent, canActivate: subscriptionReservationsGuard },
{ path: 'deferred-payment', component: DeferredOfflinePaymentComponent, canActivate: subscriptionReservationsGuard },
{ path: 'processing-payment', component: ProcessingPaymentComponent, canActivate: subscriptionReservationsGuard },
{ path: 'success', component: SuccessSubscriptionComponent, canActivate: subscriptionReservationsGuard },
Expand All @@ -74,6 +76,7 @@ const routes: Routes = [
{ path: 'overview', component: OverviewComponent, canActivate: eventReservationsGuard },
{ path: 'waitingPayment', redirectTo: 'waiting-payment'},
{ path: 'waiting-payment', component: OfflinePaymentComponent, canActivate: eventReservationsGuard },
{ path: 'waiting-custom-payment', component: CustomOfflinePaymentComponent, canActivate: eventReservationsGuard },
{ path: 'deferred-payment', component: DeferredOfflinePaymentComponent, canActivate: eventReservationsGuard },
{ path: 'processing-payment', component: ProcessingPaymentComponent, canActivate: eventReservationsGuard },
{ path: 'success', component: SuccessComponent, canActivate: eventReservationsGuard },
Expand Down
4 changes: 4 additions & 0 deletions frontend/projects/public/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,13 @@ import {TicketFormComponent} from './reservation/ticket-form/ticket-form.compone
import {CountdownComponent} from './countdown/countdown.component';
import {BannerCheckComponent} from './banner-check/banner-check.component';
import {OfflinePaymentComponent} from './reservation/offline-payment/offline-payment.component';
import {CustomOfflinePaymentComponent} from './reservation/custom-offline-payment/custom-offline-payment.component';
import {OfflinePaymentProxyComponent} from './payment/offline-payment-proxy/offline-payment-proxy.component';
import {OnsitePaymentProxyComponent} from './payment/onsite-payment-proxy/onsite-payment-proxy.component';
import {PaypalPaymentProxyComponent} from './payment/paypal-payment-proxy/paypal-payment-proxy.component';
import {StripePaymentProxyComponent} from './payment/stripe-payment-proxy/stripe-payment-proxy.component';
import {SaferpayPaymentProxyComponent} from './payment/saferpay-payment-proxy/saferpay-payment-proxy.component';
import {CustomOfflinePaymentProxyComponent} from 'projects/public/src/app/payment/custom-offline-payment-proxy/custom-offline-payment-proxy.component';
import {ProcessingPaymentComponent} from './reservation/processing-payment/processing-payment.component';
import {SummaryTableComponent} from './reservation/summary-table/summary-table.component';
import {InvoiceFormComponent} from './reservation/invoice-form/invoice-form.component';
Expand Down Expand Up @@ -167,11 +169,13 @@ export function InitUserService(userService: UserService): () => Promise<boolean
CountdownComponent,
BannerCheckComponent,
OfflinePaymentComponent,
CustomOfflinePaymentComponent,
OfflinePaymentProxyComponent,
OnsitePaymentProxyComponent,
PaypalPaymentProxyComponent,
StripePaymentProxyComponent,
SaferpayPaymentProxyComponent,
CustomOfflinePaymentProxyComponent,
ProcessingPaymentComponent,
SummaryTableComponent,
InvoiceFormComponent,
Expand Down
8 changes: 6 additions & 2 deletions frontend/projects/public/src/app/model/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ export class PaymentProxyWithParameters {
export type EventFormat = 'IN_PERSON' | 'ONLINE' | 'HYBRID';

export type PaymentMethod = 'CREDIT_CARD' | 'PAYPAL' | 'IDEAL' | 'BANK_TRANSFER' | 'ON_SITE'
| 'APPLE_PAY' | 'BANCONTACT' | 'ING_HOME_PAY' | 'BELFIUS' | 'PRZELEWY_24' | 'KBC' | 'NONE';
export type PaymentProxy = 'STRIPE' | 'ON_SITE' | 'OFFLINE' | 'PAYPAL' | 'MOLLIE' | 'SAFERPAY';
| 'APPLE_PAY' | 'BANCONTACT' | 'ING_HOME_PAY' | 'BELFIUS' | 'PRZELEWY_24' | 'KBC' | 'ETRANSFER' | 'NONE';
export type PaymentProxy = 'STRIPE' | 'ON_SITE' | 'OFFLINE' | 'PAYPAL' | 'MOLLIE' | 'SAFERPAY' | 'CUSTOM_OFFLINE';
export interface PaymentMethodDetails {
labelKey: string;
icon: [IconPrefix, IconName];
Expand Down Expand Up @@ -147,6 +147,10 @@ export const paymentMethodDetails: {[key in PaymentMethod]: PaymentMethodDetails
labelKey: 'reservation-page.payment-method.kbc',
icon: ['fas', 'money-check-alt']
},
'ETRANSFER': {
labelKey: 'reservation-page.payment-method.etransfer',
icon: ['fas', 'exchange-alt']
},
'NONE': {
labelKey: null,
icon: ['fas', 'exchange-alt']
Expand Down
2 changes: 1 addition & 1 deletion frontend/projects/public/src/app/model/reservation-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class SummaryRow {
}

export type ReservationStatus = 'PENDING' | 'IN_PAYMENT' | 'EXTERNAL_PROCESSING_PAYMENT' |
'WAITING_EXTERNAL_CONFIRMATION' | 'OFFLINE_PAYMENT' | 'DEFERRED_OFFLINE_PAYMENT' |
'WAITING_EXTERNAL_CONFIRMATION' | 'OFFLINE_PAYMENT' | 'CUSTOM_OFFLINE_PAYMENT' | 'DEFERRED_OFFLINE_PAYMENT' |
'OFFLINE_FINALIZING' | 'FINALIZING' | 'COMPLETE' | 'STUCK' | 'CANCELLED' |
'CREDIT_NOTE_ISSUED' | 'NOT_FOUND';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {SimplePaymentProvider} from '../payment-provider';

export class CustomOfflinePaymentProvider extends SimplePaymentProvider {
override get paymentMethodDeferred(): boolean {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div *ngIf="matchProxyAndMethod" class="row mt-2 mb-2">
<div class="col-12">
<div class="text-body-secondary" translate="reservation-page.payment.custom-offline.description"></div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core';

import {PaymentProvider} from '../payment-provider';
import {UntypedFormGroup} from '@angular/forms';
import { CustomOfflinePaymentProvider } from './custom-offline-payment-provider';
import {PaymentMethod, PaymentProxy} from '../../model/event';

@Component({
selector: 'app-custom-offline-payment-proxy',
templateUrl: './custom-offline-payment-proxy.component.html'
})
export class CustomOfflinePaymentProxyComponent implements OnChanges {

@Input()
method?: PaymentMethod;

@Input()
proxy?: PaymentProxy;

@Input()
parameters?: {[key: string]: any};

@Input()
overviewForm?: UntypedFormGroup;

@Output()
paymentProvider: EventEmitter<PaymentProvider> = new EventEmitter<PaymentProvider>();

private compatibleMethods: PaymentMethod[] | string[] = ['ETRANSFER'];

constructor() { }

ngOnChanges(changes: SimpleChanges): void {
if (this.matchProxyAndMethod && changes['method']) {
this.paymentProvider.emit(new CustomOfflinePaymentProvider());
}
}

public get matchProxyAndMethod(): boolean {
if (!this.method) {
return false;
}

return (this.compatibleMethods.includes(this.method)) && this.proxy === 'CUSTOM_OFFLINE';
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<app-reservation>

<div class="mb-2 mt-2 center-block" *ngIf="purchaseContext && reservationInfo">

<div class="alert alert-warning mt-2 mb-2 text-center">
<h2 *ngIf="reservationFinalized"><fa-icon [icon]="['fas', 'exclamation-triangle']" a11yRole="presentation"></fa-icon>{{' '}}<span [innerHTML]="'reservation-page-waiting.title' | translate: {'0': reservationInfo.formattedExpirationDate[translate.currentLang]}"></span></h2>
<h2 *ngIf="!reservationFinalized" translate="reservation-page-complete.reservation.finalization-in-progress"></h2>
</div>

<div class="page-header">
<h2 translate="reservation-page.title"></h2>
</div>
<app-summary-table [purchaseContext]="purchaseContext" [reservationInfo]="reservationInfo"></app-summary-table>

<ng-container *ngIf="reservationFinalized">

<h4 [innerHTML]="'reservation-page-waiting.required-steps'|translate" class="mt-5"></h4>
<h4>CUSTOM OFFLINE PAYMENTS PAGE</h4>

<ng-template #basicInstruction>
<li *ngIf="purchaseContext.bankAccountOwner.length > 0" class="mb-2">
<p class="no-margin-bottom" [innerHTML]="'reservation-page-waiting.required-steps.1.with-bank-account-owner' | translate: {'0': purchaseContext.currency+' '+reservationInfo.orderSummary.totalPrice, '1': purchaseContext.bankAccount}"></p>
<p class="bank-account-owner-info" *ngFor="let ownerLine of purchaseContext.bankAccountOwner">{{ownerLine}}</p>
<p class="no-margin-bottom" [innerHTML]="'reservation-page-waiting.required-steps.1.with-bank-account-owner.2' | translate: {'0': paymentReason}"></p>
</li>
<li *ngIf="purchaseContext.bankAccountOwner.length === 0" class="mb-2"
[innerHTML]="'reservation-page-waiting.required-steps.1' | translate: {'0': purchaseContext.currency+' '+reservationInfo.orderSummary.totalPrice, '1': purchaseContext.bankAccount, '2': paymentReason}">
</li>
</ng-template>
<ng-container *ngIf="purchaseContext.offlinePaymentConfiguration?.showOnlyBasicInstructions">
<ul class="mt-3 list-unstyled">
<ng-container *ngTemplateOutlet="basicInstruction"></ng-container>
</ul>
</ng-container>
<ng-container *ngIf="!purchaseContext.offlinePaymentConfiguration?.showOnlyBasicInstructions">
<ol class="mt-3">
<ng-container *ngTemplateOutlet="basicInstruction"></ng-container>
<li class="mb-2" [innerHTML]="'reservation-page-waiting.required-steps.2' | translate: {'0': purchaseContext.organizationEmail, '1': reservationInfo.id}"></li>
<li class="mb-2" [innerHTML]="'reservation-page-waiting.required-steps.3' | translate"></li>
</ol>
</ng-container>
</ng-container>


<div class="text-center mt-5">
<h4 [innerHTML]="'reservation-page-waiting.questions' | translate: {'0': purchaseContext.organizationEmail, '1': reservationInfo.id}"></h4>
</div>

<div class="text-center text-body-secondary mt-5">{{'reservation-page-complete.order-information' | translate: {'0': reservationId, '1': reservationInfo.firstName + ' ' + reservationInfo.lastName} }}</div>
<hr>
<div class="row d-flex justify-content-between mobile-add-margin-bottom">
<div class="col-md-5 col-12 order-md-1" *ngIf="invoiceAvailable">
<a [href]="'/api/v2/public/' + purchaseContextType + '/' + publicIdentifier + '/reservation/' + reservationInfo.id + '/invoice'" class="btn btn-success block-button" target="_blank" translate="reservation-page-complete.download-your-invoice"></a>
</div>
<div class="col-md-5 col-12 order-md-1 text-center" *ngIf="!purchaseContext.invoicingConfiguration.userCanDownloadReceiptOrInvoice"><p translate="reservation-page-waiting.invoice-will-be-sent"></p></div>
<div class="col-md-5 col-12 order-md-0"><a [href]="purchaseContext.websiteUrl" class="btn btn-light block-button" translate="to-event-site"></a></div>
</div>
</div>

</app-reservation>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.bank-account-owner-info {
padding-left: 1em;
margin-bottom: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {Component, OnInit} from '@angular/core';
import {ReservationService} from '../../shared/reservation.service';
import {ReservationInfo} from '../../model/reservation-info';
import {ActivatedRoute} from '@angular/router';
import {zip} from 'rxjs';
import {TranslateService} from '@ngx-translate/core';
import {I18nService} from '../../shared/i18n.service';
import {AnalyticsService} from '../../shared/analytics.service';
import {PurchaseContext} from '../../model/purchase-context';
import {PurchaseContextService, PurchaseContextType} from '../../shared/purchase-context.service';
import {pollReservationStatus} from '../../shared/util';

@Component({
selector: 'app-custom-offline-payment',
templateUrl: './custom-offline-payment.component.html',
styleUrls: ['./custom-offline-payment.component.scss']
})
export class CustomOfflinePaymentComponent implements OnInit {

reservationInfo?: ReservationInfo;
purchaseContextType?: PurchaseContextType;
publicIdentifier?: string;
reservationId?: string;
paymentReason?: string;

purchaseContext?: PurchaseContext;

reservationFinalized?: boolean;

constructor(
private route: ActivatedRoute,
private reservationService: ReservationService,
public translate: TranslateService,
private i18nService: I18nService,
private analytics: AnalyticsService,
private purchaseContextService: PurchaseContextService) { }

public ngOnInit(): void {
zip(this.route.data, this.route.params).subscribe(([data, params]) => {
this.purchaseContextType = data['type'];
this.publicIdentifier = params[data['publicIdentifierParameter']];
this.reservationId = params['reservationId'];
zip(
this.purchaseContextService.getContext(this.purchaseContextType, this.publicIdentifier),
this.reservationService.getReservationInfo(this.reservationId)
).subscribe(([ev, reservationInfo]) => {
console.log("reservationInfo:", reservationInfo);
console.log("purchaseContext:", ev);
this.purchaseContext = ev;
this.reservationInfo = reservationInfo;

this.paymentReason = `<mark>${this.reservationInfo.shortId}</mark>`;

this.i18nService.setPageTitle('reservation-page-waiting.header.title', ev);
this.analytics.pageView(ev.analyticsConfiguration);
if (this.reservationInfo.status === 'OFFLINE_FINALIZING') {
this.reservationFinalized = false;
pollReservationStatus(this.reservationId, this.reservationService, res => {
if (res.status === 'DEFERRED_OFFLINE_PAYMENT') {
// redirect to deferred payment. Reload the page
location.reload();
}
this.reservationInfo = res;
this.reservationFinalized = true;
});
} else {
this.reservationFinalized = true;
}
});
});
}

get invoiceAvailable(): boolean {
return this.reservationFinalized
&& this.purchaseContext.invoicingConfiguration.userCanDownloadReceiptOrInvoice
&& this.reservationInfo.invoiceNumber !== null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ <h4 class="mt-2">
[method]="selectedPaymentMethod"
(paymentProvider)="registerCurrentPaymentProvider($event)">
</app-saferpay-payment-proxy>
<app-custom-offline-payment-proxy
[proxy]="selectedPaymentProxy"
[method]="selectedPaymentMethod"
(paymentProvider)="registerCurrentPaymentProvider($event)">
</app-custom-offline-payment-proxy>
</div>

</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {SuccessComponent} from './success/success.component';
import {OverviewComponent} from './overview/overview.component';
import {BookingComponent} from './booking/booking.component';
import {OfflinePaymentComponent} from './offline-payment/offline-payment.component';
import {CustomOfflinePaymentComponent} from './custom-offline-payment/custom-offline-payment.component';
import {ProcessingPaymentComponent} from './processing-payment/processing-payment.component';
import {NotFoundComponent} from './not-found/not-found.component';
import {ErrorComponent} from './error/error.component';
Expand Down Expand Up @@ -48,6 +49,8 @@ function getRouteFromComponent(component: any, type: PurchaseContextType, public
return [type, publicIdentifier, 'reservation', reservationId, 'success'];
} else if (component === OfflinePaymentComponent) {
return [type, publicIdentifier, 'reservation', reservationId, 'waiting-payment'];
} else if (component === CustomOfflinePaymentComponent) {
return [type, publicIdentifier, 'reservation', reservationId, 'waiting-custom-payment'];
} else if (component === DeferredOfflinePaymentComponent) {
return [type, publicIdentifier, 'reservation', reservationId, 'deferred-payment'];
} else if (component === ProcessingPaymentComponent) {
Expand All @@ -70,6 +73,7 @@ function getCorrespondingController(type: PurchaseContextType, status: Reservati
case 'OFFLINE_PAYMENT':
case 'OFFLINE_FINALIZING':
return OfflinePaymentComponent;
case 'CUSTOM_OFFLINE_PAYMENT': return CustomOfflinePaymentComponent;
case 'DEFERRED_OFFLINE_PAYMENT': return DeferredOfflinePaymentComponent;
case 'EXTERNAL_PROCESSING_PAYMENT':
case 'WAITING_EXTERNAL_CONFIRMATION': return ProcessingPaymentComponent;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/alfio/controller/IndexController.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public ResponseEntity<String> replyToK8s() {
"/event/{eventShortName}/reservation/{reservationId}/overview",
"/event/{eventShortName}/reservation/{reservationId}/waitingPayment",
"/event/{eventShortName}/reservation/{reservationId}/waiting-payment",
"/event/{eventShortName}/reservation/{reservationId}/waiting-custom-payment",
"/event/{eventShortName}/reservation/{reservationId}/deferred-payment",
"/event/{eventShortName}/reservation/{reservationId}/processing-payment",
"/event/{eventShortName}/reservation/{reservationId}/success",
Expand All @@ -150,6 +151,7 @@ public ResponseEntity<String> replyToK8s() {
"/subscription/{subscriptionId}/reservation/{reservationId}/overview",
"/subscription/{subscriptionId}/reservation/{reservationId}/waitingPayment",
"/subscription/{subscriptionId}/reservation/{reservationId}/waiting-payment",
"/subscription/{subscriptionId}/reservation/{reservationId}/waiting-custom-payment",
"/subscription/{subscriptionId}/reservation/{reservationId}/deferred-payment",
"/subscription/{subscriptionId}/reservation/{reservationId}/processing-payment",
"/subscription/{subscriptionId}/reservation/{reservationId}/success",
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/alfio/manager/ReservationFinalizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ private void completeReservation(FinalizeReservation finalizeReservation) {
ticketReservationRepository.setMetadata(reservationId, metadata.withFinalized(true));
Locale locale = LocaleUtil.forLanguageTag(reservation.getUserLanguage());
List<Ticket> tickets = null;
if(paymentProxy != PaymentProxy.OFFLINE) {
if(paymentProxy != PaymentProxy.OFFLINE && paymentProxy != PaymentProxy.CUSTOM_OFFLINE) {
ticketReservationRepository.updateReservationStatus(reservationId, COMPLETE.name());
tickets = acquireItems(paymentProxy, reservationId, spec.getEmail(), spec.getCustomerName(), spec.getLocale().getLanguage(), spec.getBillingAddress(), spec.getCustomerReference(), spec.getPurchaseContext(), finalizeReservation.isSendTickets());
extensionManager.handleReservationConfirmation(reservation, ticketReservationRepository.getBillingDetailsForReservation(reservationId), spec.getPurchaseContext());
Expand Down Expand Up @@ -435,7 +435,8 @@ public void confirmOfflinePayment(Event event,
if (!metadata.isReadyForConfirmation()) {
throw new IncompatibleStateException("Reservation is not ready to be confirmed");
}
Validate.isTrue(ticketReservation.getPaymentMethod() == PaymentProxy.OFFLINE, "invalid payment method");
Validate.isTrue(ticketReservation.getPaymentMethod() == PaymentProxy.OFFLINE
|| ticketReservation.getPaymentMethod() == PaymentProxy.CUSTOM_OFFLINE, "invalid payment method");
Validate.isTrue(ticketReservation.isPendingOfflinePayment(), "invalid status");


Expand Down
Loading

0 comments on commit 4e21a69

Please sign in to comment.