From e355726e09ae4057e683b7babac82db25fcf0852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sch=C3=A4chinger?= Date: Wed, 11 Aug 2021 00:04:07 +0200 Subject: [PATCH] feat: portokasse top up implementation --- README.md | 25 ++++++++++++ src/Internetmarke.ts | 9 ++++- src/portokasse/Error.ts | 3 ++ src/portokasse/Service.ts | 67 +++++++++++++++++++++++++-------- test/portokasse/Service.spec.ts | 37 ++++++++++++++---- 5 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 src/portokasse/Error.ts diff --git a/README.md b/README.md index 026fbc2..f0c0d94 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,31 @@ from the Portokasse service. ### Top Up Account +Top up is the main method of the Portokasse service. There are two different +payment methods: PayPal and Giropay. Giropay expects a BIC string to be also +passed to the method. Both methods will result in a `redirect` link that should +be called by the user to finish the top up request. + +**Info:** The minimum amount to top up is EUR 10,00 which can be defined as an +`Amount` object or a raw number in Euro Cents. + +**PayPal top up** + +```typescript +const amount = { value: 10, currency: 'EUR' }; // or: const amount = 1000; +const payment = await internetmarke.topUp(amount, PaymentMethod.PayPal); +// payment: { code: 'OK', redirect: 'https://paypal.com/...' } +``` + +**GiroPay top up** + +```typescript +const amount = { value: 10, currency: 'EUR' }; // or: const amount = 1000; +const bic = 'HOLVDEB1XXX'; +const payment = await internetmarke.topUp(amount, PaymentMethod.GiroPay, bic); +// payment: { code: 'OK', redirect: 'https://giropay.de/...' } +``` + ## ProdWS (Product Service) The product list contains all available vouchers that can be ordered. They are diff --git a/src/Internetmarke.ts b/src/Internetmarke.ts index d905a13..d374606 100644 --- a/src/Internetmarke.ts +++ b/src/Internetmarke.ts @@ -23,6 +23,7 @@ import { Amount, Product } from './prodWs/product'; import { ProductService, ProductServiceOptions, ProdWS } from './prodWs/Service'; import { PaymentMethod, + PaymentResponse, Portokasse, PortokasseService, PortokasseServiceOptions @@ -318,13 +319,17 @@ export class Internetmarke implements OneClickForApp, Portokasse, ProdWS { return this.portokasseService; } - public topUp(amount: Amount | number, paymentMethod: PaymentMethod): Promise { + public topUp( + amount: Amount | number, + paymentMethod: PaymentMethod, + bic?: string + ): Promise { this.checkServiceInit( this.portokasseService, 'Cannot get balance before initializing portokasse service' ); - return this.portokasseService.topUp(amount, paymentMethod); + return this.portokasseService.topUp(amount, paymentMethod, bic); } protected init(): void { diff --git a/src/portokasse/Error.ts b/src/portokasse/Error.ts new file mode 100644 index 0000000..e42ca71 --- /dev/null +++ b/src/portokasse/Error.ts @@ -0,0 +1,3 @@ +import { InternetmarkeError } from '../Error'; + +export class PortokasseError extends InternetmarkeError {} diff --git a/src/portokasse/Service.ts b/src/portokasse/Service.ts index 8ee3455..1b85556 100644 --- a/src/portokasse/Service.ts +++ b/src/portokasse/Service.ts @@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify'; import { CookieJar } from 'tough-cookie'; import { TYPES } from '../di/types'; import { UserError } from '../Error'; +import { PortokasseError } from './Error'; import { Amount } from '../prodWs/product'; import { RestService } from '../services/Rest'; import { User, UserCredentials, UserInfo } from '../User'; @@ -13,15 +14,25 @@ export enum PaymentMethod { Paypal = 'PAYPAL' } +export interface PaymentResponse { + code: string; // 'OK' + redirect: string; // paypal url +} + export interface PortokasseServiceOptions { user: UserCredentials; } export interface Portokasse { - topUp(amount: Amount | number, paymentMethod: PaymentMethod): Promise; + getUserInfo(): Promise; + topUp( + amount: Amount | number, + paymentMethod: PaymentMethod, + bic?: string + ): Promise; } -export const BASE_URL = 'https://portokasse.deutschepost.de/portokasse'; +const BASE_URL = 'https://portokasse.deutschepost.de/portokasse'; @injectable() export class PortokasseService extends RestService implements Portokasse { @@ -45,7 +56,7 @@ export class PortokasseService extends RestService implements Portokasse { } public isInitialized(): boolean { - return !!this.cookieJar; + return this.user.isAuthenticated(); } public async getUserInfo(): Promise { @@ -61,10 +72,20 @@ export class PortokasseService extends RestService implements Portokasse { } public async topUp( - _amount: Amount | number, - _paymentMethod: PaymentMethod - ): Promise { - return false; + amount: Amount | number, + paymentMethod: PaymentMethod, + bic?: string + ): Promise { + const data: any = { + amount: 'number' === typeof amount ? amount : (amount as Amount).value * 100, + paymentMethod + }; + + if (PaymentMethod.GiroPay === paymentMethod) { + data.bic = bic; + } + + return this.request('POST', '/api/v1/payments', data); } private async login(): Promise { @@ -84,19 +105,30 @@ export class PortokasseService extends RestService implements Portokasse { withCredentials: true }; + const isLogin = '/login' === path; + if (data) { - const encodedData: string[] = []; - for (let prop in data) { - encodedData.push(`${prop}=${encodeURIComponent(data[prop])}`); + if (isLogin) { + const encodedData: string[] = []; + for (let prop in data) { + encodedData.push(`${prop}=${encodeURIComponent(data[prop])}`); + } + + options.data = encodedData.join('&'); + } else { + options.data = data; } - - options.data = encodedData.join('&'); } if (this.csrf) { options.headers = { - 'X-CSRF-TOKEN': this.csrf, - 'Content-Type': 'application/x-www-form-urlencoded' + 'X-CSRF-TOKEN': this.csrf }; + + if (data) { + options.headers['Content-Type'] = isLogin + ? 'application/x-www-form-urlencoded' + : 'application/json'; + } } try { @@ -112,7 +144,12 @@ export class PortokasseService extends RestService implements Portokasse { return res.data; } catch (e) { - return e.response?.data || null; + const error = new PortokasseError( + `Error from Portokasse: ${e.response?.data.code || 'Unknown'}` + ); + (error as any).response = e.response || e.message; + + throw error; } } } diff --git a/test/portokasse/Service.spec.ts b/test/portokasse/Service.spec.ts index 06c3184..e89be06 100644 --- a/test/portokasse/Service.spec.ts +++ b/test/portokasse/Service.spec.ts @@ -4,6 +4,7 @@ import { UserError } from '../../src/Error'; import { PaymentMethod, PortokasseService } from '../../src/portokasse/Service'; import { userCredentials } from '../1c4a/helper'; import { User } from '../../src/User'; +import { PortokasseError } from '../../src/portokasse/Error'; describe('Portokasse Service', () => { let service: PortokasseService; @@ -38,7 +39,7 @@ describe('Portokasse Service', () => { }); it('should init with minimal options', async () => { - expect(service.init({ user: userCredentials })).to.eventually.be.fulfilled; + await service.init({ user: userCredentials }); expect(service.isInitialized()).to.be.true; }); }); @@ -53,9 +54,7 @@ describe('Portokasse Service', () => { } }); - const info = await service.getUserInfo(); - - expect(info.isAuthenticated).to.be.false; + expect(service.getUserInfo()).to.eventually.be.rejectedWith(PortokasseError); }); it('should retrieve user balance', async () => { @@ -77,10 +76,34 @@ describe('Portokasse Service', () => { }); describe('topUp', () => { - it('should add tests once topup is implemented', async () => { - const res = await service.topUp(1000, PaymentMethod.GiroPay); + it('should top up with GiroPay', async () => { + moxios.stubOnce('post', /\/payments$/, { + status: 200, + headers: {}, + response: { + code: 'OK', + redirect: 'http://localhost' + } + }); + + const res = await service.topUp(1000, PaymentMethod.GiroPay, 'XXXXDEXXXX'); + + expect(res).to.exist; + }); + + it('should top up with Paypal', async () => { + moxios.stubOnce('post', /\/payments$/, { + status: 200, + headers: {}, + response: { + code: 'OK', + redirect: 'http://localhost' + } + }); + + const res = await service.topUp(1000, PaymentMethod.Paypal); - expect(res).to.be.false; + expect(res).to.exist; }); }); });