Skip to content

Commit

Permalink
Payment service simplification refactor (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
mic-max authored Jul 8, 2022
1 parent da2b1db commit fa796f8
Show file tree
Hide file tree
Showing 9 changed files with 5,755 additions and 251 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
.nb-gradle-properties
.swp
.DS_Store
package-lock.json
\#*\#

# Eclipse
Expand Down
6 changes: 6 additions & 0 deletions src/paymentservice/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.dockerignore
Dockerfile
package.json
package-lock.json
README.md
17 changes: 9 additions & 8 deletions src/paymentservice/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,26 @@
# See the License for the specific language governing permissions and
# limitations under the License.

FROM node:16-alpine AS base

FROM base AS builder
FROM node:16-alpine AS build

WORKDIR /usr/src/app/

COPY ./src/paymentservice/package.json ./
COPY ./src/paymentservice/package*.json ./

RUN npm install --only=production
RUN npm ci --omit=dev

# -----------------------------------------------------------------------------

FROM base
FROM node:16-alpine

USER node
WORKDIR /usr/src/app/
ENV NODE_ENV production

COPY --from=builder /usr/src/app/node_modules/ ./node_modules/
COPY --chown=node:node --from=build /usr/src/app/node_modules/ ./node_modules/
COPY ./src/paymentservice/ ./
COPY ./pb/ ./proto/
COPY ./pb/demo.proto ./

EXPOSE ${PAYMENT_SERVICE_PORT}

ENTRYPOINT [ "node", "--require", "./tracing.js", "./index.js" ]
109 changes: 35 additions & 74 deletions src/paymentservice/charge.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,85 +12,46 @@
// See the License for the specific language governing permissions and
// limitations under the License.

const cardValidator = require('simple-card-validator');
const { v4: uuidv4 } = require('uuid');
const pino = require('pino');
const opentelemetry = require('@opentelemetry/api');
const tracer = opentelemetry.trace.getTracer("paymentservice");

const logger = pino({
name: 'paymentservice-charge',
messageKey: 'message',
levelKey: 'severity',
useLevelLabels: true
});


class CreditCardError extends Error {
constructor (message) {
super(message);
this.code = 400; // Invalid argument error
}
}

class InvalidCreditCard extends CreditCardError {
constructor (cardType) {
super(`Credit card info is invalid`);
}
}

class UnacceptedCreditCard extends CreditCardError {
constructor (cardType) {
super(`Sorry, we cannot process ${cardType} credit cards. Only VISA or MasterCard is accepted.`);
}
}

class ExpiredCreditCard extends CreditCardError {
constructor (number, month, year) {
super(`Your credit card (ending ${number.substr(-4)}) expired on ${month}/${year}`);
}
}

/**
* Verifies the credit card number and (pretend) charges the card.
*
* @param {*} request
* @return transaction_id - a random uuid v4.
*/
module.exports = function charge (request) {
// create and start span
const span = tracer.startSpan("charge")

const { amount, credit_card: creditCard } = request;
const cardNumber = creditCard.credit_card_number;
const cardInfo = cardValidator(cardNumber);
const {
card_type: cardType,
valid
} = cardInfo.getCardDetails();
// Npm
const opentelemetry = require('@opentelemetry/api')
const cardValidator = require('simple-card-validator')
const pino = require('pino')
const { v4: uuidv4 } = require('uuid')

// Setup
const logger = pino()
const tracer = opentelemetry.trace.getTracer('paymentservice')

// Functions
module.exports.charge = request => {
const span = tracer.startSpan('charge')

const { amount, creditCard } = request
const cardNumber = creditCard.creditCardNumber
const card = cardValidator(cardNumber)
const {card_type: cardType, valid } = card.getCardDetails()
span.setAttributes({
"app.payment.charge.cardType": cardType,
"app.payment.charge.valid": valid
'app.payment.charge.cardType': cardType,
'app.payment.charge.valid': valid
})

if (!valid) { throw new InvalidCreditCard(); }
if (!valid)
throw new Error('Credit card info is invalid.')

// Only VISA and mastercard is accepted, other card types (AMEX, dinersclub) will
// throw UnacceptedCreditCard error.
if (!(cardType === 'visa' || cardType === 'mastercard')) { throw new UnacceptedCreditCard(cardType); }
if (!['visa', 'mastercard'].includes(cardType))
throw new Error(`Sorry, we cannot process ${cardType} credit cards. Only VISA or MasterCard is accepted.`)

// Also validate expiration is > today.
const currentMonth = new Date().getMonth() + 1;
const currentYear = new Date().getFullYear();
const { credit_card_expiration_year: year, credit_card_expiration_month: month } = creditCard;
if ((currentYear * 12 + currentMonth) > (year * 12 + month)) { throw new ExpiredCreditCard(cardNumber.replace('-', ''), month, year); }
const currentMonth = new Date().getMonth() + 1
const currentYear = new Date().getFullYear()
const { credit_card_expiration_year: year, credit_card_expiration_month: month } = creditCard
const lastFourDigits = cardNumber.substr(-4)
if ((currentYear * 12 + currentMonth) > (year * 12 + month))
throw new Error(`The credit card (ending ${lastFourDigits}) expired on ${month}/${year}.`)

logger.info(`Transaction processed: ${cardType} ending ${cardNumber.substr(-4)} \
Amount: ${amount.currency_code}${amount.units}.${amount.nanos}`);
span.setAttribute('app.payment.charged', true)
span.end()

span.setAttribute("app.payment.charged", true);
// a manually created span needs to be ended
span.end();
logger.info(`Transaction processed: ${cardType} ending ${lastFourDigits} | Amount: ${amount.units}.${amount.nanos} ${amount.currencyCode}`)

return { transaction_id: uuidv4() };
};
return { transaction_id: uuidv4() }
}
102 changes: 75 additions & 27 deletions src/paymentservice/index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,75 @@
/*
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

const path = require('path');
const HipsterShopServer = require('./server');

const PORT = process.env['PAYMENT_SERVICE_PORT'];
const PROTO_PATH = path.join(__dirname, '/proto/');

const server = new HipsterShopServer(PROTO_PATH, PORT);

server.listen();
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Npm
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')
const health = require('grpc-health-check')
const opentelemetry = require('@opentelemetry/api')
const pino = require('pino')

// Local
const charge = require('./charge')

// Functions
function chargeServiceHandler(call, callback) {
const span = opentelemetry.trace.getSpan(opentelemetry.context.active())

try {
const amount = call.request.amount
span.setAttributes({
'app.payment.currency': amount.currency_code,
'app.payment.cost': parseFloat(`${amount.units}.${amount.nanos}`)
})
logger.info(`PaymentService#Charge invoked by: ${JSON.stringify(call.request)}`)

const response = charge.charge(call.request)
callback(null, response)

} catch (err) {
logger.warn(err)

span.recordException(err)
span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR })

callback(err)
}
}

// Functions
async function closeGracefully(signal) {
server.forceShutdown()
process.kill(process.pid, signal)
}

// Main
const logger = pino()
const port = process.env['PAYMENT_SERVICE_PORT']
const hipsterShopPackage = grpc.loadPackageDefinition(protoLoader.loadSync('demo.proto'))
const server = new grpc.Server()

server.addService(health.service, new health.Implementation({
'': proto.grpc.health.v1.HealthCheckResponse.ServingStatus.SERVING
}))

server.addService(hipsterShopPackage.hipstershop.PaymentService.service, { charge: chargeServiceHandler })

server.bindAsync(`0.0.0.0:${port}`, grpc.ServerCredentials.createInsecure(), () => {
logger.info(`PaymentService gRPC server started on port ${port}`)
server.start()
}
)

process.once('SIGINT', closeGracefully)
process.once('SIGTERM', closeGracefully)
Loading

0 comments on commit fa796f8

Please sign in to comment.