diff --git a/README.md b/README.md index d913ec4f..bf795ab8 100644 --- a/README.md +++ b/README.md @@ -46,22 +46,43 @@ export default function getPaymentData() { province: 'New South Wales', }, cart: { + currency: 'AUD', + grandTotal: { + integerAmount: 12000, + }, + handling: { + integerAmount: 0, + }, + shipping: { + integerAmount: 1000, + }, + subtotal: { + integerAmount: 10000, + }, + taxTotal: { + integerAmount: 1000, + }, items: [ { id: '123', integerAmount: 10000, + integerAmountAfterDiscount: 10000, + integerDiscount: 0, + integerTax: 1000, name: 'Cheese', quantity: 1, sku: '123456789', + type: 'ItemPhysicalEntity', }, ], }, customer: { customerId: '123', - email: 'customer@bigcommerce.com', + email: 'email@bigcommerce.com', firstName: 'Foo', lastName: 'Bar', - locale: 'en-AU', + name: 'Foo Bar', + phoneNumber: '98765432', }, order: { currency: 'AUD', @@ -119,6 +140,12 @@ export default function getPaymentData() { }, source: 'bcapp-checkout-uco', store: { + cartLink: '/cart', + checkoutLink: '/checkout', + countryCode: 'AU', + currencyCode: 'AUD', + orderConfirmationLink: '/order-confirmation', + shopPath: '/', storeHash: 's123456789', storeId: '100', storeLanguage: 'en-AU', diff --git a/src/payment/v2/payment-mappers/cart-mapper.js b/src/payment/v2/payment-mappers/cart-mapper.js new file mode 100644 index 00000000..f7d4e959 --- /dev/null +++ b/src/payment/v2/payment-mappers/cart-mapper.js @@ -0,0 +1,77 @@ +import { omitNil } from '../../../common/utils'; + +export default class CartMapper { + /** + * @returns {CartMapper} + */ + static create() { + return new CartMapper(); + } + + /** + * @param {PaymentRequestData} data + * @returns {Object} + */ + mapToCart(data) { + const { cart = {} } = data; + + return omitNil({ + currency_code: cart.currency, + items: this.mapToItems(data), + totals: this.mapToOrderTotals(data), + }); + } + + /** + * @private + * @param {PaymentRequestData} data + * @returns {Object[]} + */ + mapToItems(data) { + const { cart = { items: [] } } = data; + + return cart.items.map(itemData => omitNil({ + discount_amount: itemData.integerDiscount, + name: itemData.name, + price: itemData.integerAmount, + quantity: itemData.quantity, + sku: itemData.sku, + tax_amount: itemData.integerTax, + amount: itemData.integerAmountAfterDiscount, + type: this.mapToType(itemData), + })); + } + + /** + * @private + * @param {PaymentRequestData} data + * @returns {Object} + */ + mapToOrderTotals(data) { + const { cart = {} } = data; + + return omitNil({ + discount_total: cart.discount ? cart.discount.integerAmount : null, + grand_total: cart.grandTotal ? cart.grandTotal.integerAmount : null, + shipping_total: cart.shipping ? cart.shipping.integerAmount : null, + subtotal: cart.subtotal ? cart.subtotal.integerAmount : null, + surcharge_total: cart.handling ? cart.handling.integerAmount : null, + tax_total: cart.taxTotal ? cart.taxTotal.integerAmount : null, + }); + } + + /** + * @private + * @param {Object} itemData + * @returns {Object} + */ + mapToType(itemData) { + const types = { + ItemPhysicalEntity: 'physical', + ItemDigitalEntity: 'digital', + ItemGiftCertificateEntity: 'gift_card', + }; + + return types[itemData.type]; + } +} diff --git a/src/payment/v2/payment-mappers/gateway-mapper.js b/src/payment/v2/payment-mappers/gateway-mapper.js new file mode 100644 index 00000000..32ebe125 --- /dev/null +++ b/src/payment/v2/payment-mappers/gateway-mapper.js @@ -0,0 +1,37 @@ +import { omitNil } from '../../../common/utils'; +import PaymentMethodIdMapper from '../../payment-method-mappers/payment-method-id-mapper'; + +export default class GatewayMapper { + /** + * @returns {GatewayMapper} + */ + static create() { + const paymentMethodIdMapper = PaymentMethodIdMapper.create(); + + return new GatewayMapper(paymentMethodIdMapper); + } + + /** + * @param {PaymentMethodIdMapper} paymentMethodIdMapper + * @returns {void} + */ + constructor(paymentMethodIdMapper) { + /** + * @private + * @type {PaymentMethodIdMapper} + */ + this.paymentMethodIdMapper = paymentMethodIdMapper; + } + + /** + * @param {PaymentRequestData} data + * @returns {Object} + */ + mapToGateway(data) { + const { paymentMethod = {} } = data; + + return omitNil({ + name: this.paymentMethodIdMapper.mapToId(paymentMethod), + }); + } +} diff --git a/src/payment/v2/payment-mappers/header-mapper.js b/src/payment/v2/payment-mappers/header-mapper.js new file mode 100644 index 00000000..7a1a89b2 --- /dev/null +++ b/src/payment/v2/payment-mappers/header-mapper.js @@ -0,0 +1,22 @@ +import { omitNil } from '../../../common/utils'; + +export default class HeaderMapper { + /** + * @returns {HeaderMapper} + */ + static create() { + return new HeaderMapper(); + } + + /** + * @param {PaymentRequestData} data + * @returns {Object} + */ + mapToHeaders(data) { + const { authToken } = data; + + return omitNil({ + Authorization: authToken, + }); + } +} diff --git a/src/payment/v2/payment-mappers/quote-mapper.js b/src/payment/v2/payment-mappers/quote-mapper.js new file mode 100644 index 00000000..a5259111 --- /dev/null +++ b/src/payment/v2/payment-mappers/quote-mapper.js @@ -0,0 +1,46 @@ +import { omitNil } from '../../../common/utils'; + +export default class QuoteMapper { + /** + * @returns {QuoteMapper} + */ + static create() { + return new QuoteMapper(); + } + + /** + * @param {PaymentRequestData} data + * @returns {Object} + */ + mapToQuote(data) { + return omitNil({ + billing_address: this.mapToAddress(data, 'billingAddress'), + shipping_address: this.mapToAddress(data, 'shippingAddress'), + }); + } + + /** + * @private + * @param {PaymentRequestData} data + * @param {string} addressKey + * @returns {Object} + */ + mapToAddress(data, addressKey) { + const { customer = {} } = data; + const address = data[addressKey] || {}; + + return omitNil({ + address_line_1: address.addressLine1, + address_line_2: address.addressLine2, + city: address.city, + company: address.company, + country_code: address.countryCode, + email: customer.email, + first_name: address.firstName, + last_name: address.lastName, + phone: address.phone, + postal_code: address.postCode, + state: address.province, + }); + } +} diff --git a/src/payment/v2/payment-mappers/store-mapper.js b/src/payment/v2/payment-mappers/store-mapper.js new file mode 100644 index 00000000..ff3fd370 --- /dev/null +++ b/src/payment/v2/payment-mappers/store-mapper.js @@ -0,0 +1,67 @@ +import { omitNil, toNumber } from '../../../common/utils'; + +export default class StoreMapper { + /** + * @returns {StoreMapper} + */ + static create() { + return new StoreMapper(); + } + + /** + * @param {PaymentRequestData} data + * @returns {Object} + */ + mapToStore(data) { + return omitNil({ + locale: this.mapToLocale(data), + store_identity: this.mapToIdentity(data), + urls: this.mapToUrls(data), + }); + } + + /** + * @private + * @param {PaymentRequestData} data + * @returns {Object} + */ + mapToLocale(data) { + const { store = {} } = data; + + return omitNil({ + country_code: store.countryCode, + currency_code: store.currencyCode, + language_code: store.storeLanguage, + }); + } + + /** + * @private + * @param {PaymentRequestData} data + * @returns {Object} + */ + mapToIdentity(data) { + const { store = {} } = data; + + return omitNil({ + id: store.storeId ? toNumber(store.storeId) : null, + name: store.storeName, + }); + } + + /** + * @private + * @param {PaymentRequestData} data + * @returns {Object} + */ + mapToUrls(data) { + const { store = {} } = data; + + return omitNil({ + cart: store.cartLink, + checkout: store.checkoutLink, + confirmation: store.orderConfirmationLink, + home: store.shopPath, + }); + } +} diff --git a/test/mocks/payment-request-data.js b/test/mocks/payment-request-data.js index 6626159d..03ce09c0 100644 --- a/test/mocks/payment-request-data.js +++ b/test/mocks/payment-request-data.js @@ -17,13 +17,36 @@ const paymentRequestDataMock = { province: 'New South Wales', }, cart: { + currency: 'AUD', + discount: { + integerAmount: 0, + }, + grandTotal: { + integerAmount: 12000, + }, + handling: { + integerAmount: 500, + }, + shipping: { + integerAmount: 1000, + }, + subtotal: { + integerAmount: 10000, + }, + taxTotal: { + integerAmount: 1000, + }, items: [ { - integerAmount: 10000, id: '123', + integerAmount: 10000, + integerAmountAfterDiscount: 10000, + integerDiscount: 0, + integerTax: 1000, name: 'Cheese', quantity: 1, sku: '123456789', + type: 'ItemPhysicalEntity', }, ], }, @@ -38,6 +61,9 @@ const paymentRequestDataMock = { order: { callbackUrl: '/order/123/payment', currency: 'AUD', + discount: { + integerAmount: 0, + }, grandTotal: { integerAmount: 12000, }, @@ -93,6 +119,12 @@ const paymentRequestDataMock = { }, source: 'bcapp-checkout-uco', store: { + cartLink: '/cart', + checkoutLink: '/checkout', + countryCode: 'AU', + currencyCode: 'AUD', + orderConfirmationLink: '/order-confirmation', + shopPath: '/', storeHash: 's123456789', storeId: '100', storeLanguage: 'en-AU', diff --git a/test/payment/v2/payment-mappers/cart-mapper.spec.js b/test/payment/v2/payment-mappers/cart-mapper.spec.js new file mode 100644 index 00000000..b92fa756 --- /dev/null +++ b/test/payment/v2/payment-mappers/cart-mapper.spec.js @@ -0,0 +1,53 @@ +import paymentRequestDataMock from '../../../mocks/payment-request-data'; +import CartMapper from '../../../../src/payment/v2/payment-mappers/cart-mapper'; + +describe('CartMapper', () => { + let data; + let cartMapper; + + beforeEach(() => { + data = paymentRequestDataMock; + cartMapper = new CartMapper(); + }); + + it('creates an instance of CartMapper', () => { + const instance = CartMapper.create(); + + expect(instance instanceof CartMapper).toBeTruthy(); + }); + + it('maps the input data into a cart object', () => { + const output = cartMapper.mapToCart(data); + + expect(output).toEqual({ + currency_code: data.cart.currency, + items: data.cart.items.map(item => ({ + discount_amount: item.integerDiscount, + name: item.name, + price: item.integerAmount, + quantity: item.quantity, + sku: item.sku, + tax_amount: item.integerTax, + amount: item.integerAmountAfterDiscount, + type: 'physical', + })), + totals: { + discount_total: data.cart.discount.integerAmount, + grand_total: data.cart.grandTotal.integerAmount, + shipping_total: data.cart.shipping.integerAmount, + subtotal: data.cart.subtotal.integerAmount, + surcharge_total: data.cart.handling.integerAmount, + tax_total: data.cart.taxTotal.integerAmount, + }, + }); + }); + + it('returns an empty object if the input does not contain cart information', () => { + const output = cartMapper.mapToCart({}); + + expect(output).toEqual({ + items: [], + totals: {}, + }); + }); +}); diff --git a/test/payment/v2/payment-mappers/gateway-mapper.spec.js b/test/payment/v2/payment-mappers/gateway-mapper.spec.js new file mode 100644 index 00000000..b6fbc24b --- /dev/null +++ b/test/payment/v2/payment-mappers/gateway-mapper.spec.js @@ -0,0 +1,42 @@ +import paymentRequestDataMock from '../../../mocks/payment-request-data'; +import GatewayMapper from '../../../../src/payment/v2/payment-mappers/gateway-mapper'; + +describe('GatewayMapper', () => { + let data; + let gatewayMapper; + let mappedData; + let paymentMethodIdMapper; + + beforeEach(() => { + data = paymentRequestDataMock; + mappedData = { + name: 'id', + }; + + paymentMethodIdMapper = { + mapToId: jasmine.createSpy('mapToId').and.returnValue(mappedData.name), + }; + + gatewayMapper = new GatewayMapper(paymentMethodIdMapper); + }); + + it('creates an instance of GatewayMapper', () => { + const instance = GatewayMapper.create(); + + expect(instance instanceof GatewayMapper).toBeTruthy(); + }); + + it('maps the input data into a gateway object', () => { + const output = gatewayMapper.mapToGateway(data); + + expect(output).toEqual({ + name: mappedData.name, + }); + }); + + it('returns an empty object if the input does not contain gateway information', () => { + paymentMethodIdMapper.mapToId.and.returnValue(null); + + expect(gatewayMapper.mapToGateway({})).toEqual({}); + }); +}); diff --git a/test/payment/v2/payment-mappers/header-mapper.spec.js b/test/payment/v2/payment-mappers/header-mapper.spec.js new file mode 100644 index 00000000..687ac89d --- /dev/null +++ b/test/payment/v2/payment-mappers/header-mapper.spec.js @@ -0,0 +1,32 @@ +import paymentRequestDataMock from '../../../mocks/payment-request-data'; +import HeaderMapper from '../../../../src/payment/v2/payment-mappers/header-mapper'; + +describe('HeaderMapper', () => { + let data; + let headerMapper; + + beforeEach(() => { + data = paymentRequestDataMock; + headerMapper = new HeaderMapper(); + }); + + it('creates an instance of HeaderMapper', () => { + const instance = HeaderMapper.create(); + + expect(instance instanceof HeaderMapper).toBeTruthy(); + }); + + it('maps the input data into a HTTP header object', () => { + const output = headerMapper.mapToHeaders(data); + + expect(output).toEqual({ + Authorization: data.authToken, + }); + }); + + it('returns an empty object if the input does not contain HTTP header information', () => { + const output = headerMapper.mapToHeaders({}); + + expect(output).toEqual({}); + }); +}); diff --git a/test/payment/v2/payment-mappers/quote-mapper.spec.js b/test/payment/v2/payment-mappers/quote-mapper.spec.js new file mode 100644 index 00000000..f45c7842 --- /dev/null +++ b/test/payment/v2/payment-mappers/quote-mapper.spec.js @@ -0,0 +1,60 @@ +import paymentRequestDataMock from '../../../mocks/payment-request-data'; +import QuoteMapper from '../../../../src/payment/v2/payment-mappers/quote-mapper'; + +describe('QuoteMapper', () => { + let data; + let quoteMapper; + + beforeEach(() => { + data = paymentRequestDataMock; + quoteMapper = new QuoteMapper(); + }); + + it('creates an instance of QuoteMapper', () => { + const instance = QuoteMapper.create(); + + expect(instance instanceof QuoteMapper).toBeTruthy(); + }); + + it('maps the input data into a quote object', () => { + const output = quoteMapper.mapToQuote(data); + + expect(output).toEqual({ + billing_address: { + address_line_1: data.billingAddress.addressLine1, + address_line_2: data.billingAddress.addressLine2, + city: data.billingAddress.city, + company: data.billingAddress.company, + country_code: data.billingAddress.countryCode, + email: data.customer.email, + first_name: data.billingAddress.firstName, + last_name: data.billingAddress.lastName, + phone: data.billingAddress.phone, + postal_code: data.billingAddress.postCode, + state: data.billingAddress.province, + }, + shipping_address: { + address_line_1: data.shippingAddress.addressLine1, + address_line_2: data.shippingAddress.addressLine2, + city: data.shippingAddress.city, + company: data.shippingAddress.company, + country_code: data.shippingAddress.countryCode, + email: data.customer.email, + first_name: data.shippingAddress.firstName, + last_name: data.shippingAddress.lastName, + phone: data.shippingAddress.phone, + postal_code: data.shippingAddress.postCode, + state: data.shippingAddress.province, + }, + }); + }); + + it('returns an empty object if the input does not contain quote information', () => { + const output = quoteMapper.mapToQuote({}); + + expect(output).toEqual({ + billing_address: {}, + shipping_address: {}, + }); + }); +}); diff --git a/test/payment/v2/payment-mappers/store-mapper.spec.js b/test/payment/v2/payment-mappers/store-mapper.spec.js new file mode 100644 index 00000000..816dd38d --- /dev/null +++ b/test/payment/v2/payment-mappers/store-mapper.spec.js @@ -0,0 +1,50 @@ +import paymentRequestDataMock from '../../../mocks/payment-request-data'; +import StoreMapper from '../../../../src/payment/v2/payment-mappers/store-mapper'; + +describe('StoreMapper', () => { + let data; + let storeMapper; + + beforeEach(() => { + data = paymentRequestDataMock; + storeMapper = new StoreMapper(); + }); + + it('creates an instance of StoreMapper', () => { + const instance = StoreMapper.create(); + + expect(instance instanceof StoreMapper).toBeTruthy(); + }); + + it('maps the input data into a store object', () => { + const output = storeMapper.mapToStore(data); + + expect(output).toEqual({ + locale: { + country_code: data.store.countryCode, + currency_code: data.store.currencyCode, + language_code: data.store.storeLanguage, + }, + store_identity: { + id: parseInt(data.store.storeId, 10), + name: data.store.storeName, + }, + urls: { + cart: data.store.cartLink, + checkout: data.store.checkoutLink, + confirmation: data.store.orderConfirmationLink, + home: data.store.shopPath, + }, + }); + }); + + it('returns an empty object if the input does not contain store information', () => { + const output = storeMapper.mapToStore({}); + + expect(output).toEqual({ + locale: {}, + store_identity: {}, + urls: {}, + }); + }); +});