diff --git a/README.md b/README.md index 637aa069..8ebae41f 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Microservice for handling PayPal / Stripe payments over AMQP transport layer +# Microservice for handling PayPal payments over AMQP transport layer [![Build Status](https://semaphoreci.com/api/v1/makeomatic/ms-payments/branches/master/shields_badge.svg)](https://semaphoreci.com/makeomatic/ms-payments) [![Code Climate](https://codeclimate.com/github/makeomatic/ms-payments/badges/gpa.svg)](https://codeclimate.com/github/makeomatic/ms-payments) @@ -14,9 +14,6 @@ Please follow [this link](https://makeomatic.github.io/ms-payments/docs/API.html ## Tests -Before running tests, you should create your "/test/.env" file with stripe private and public keys. -Details are in ["./test/.env.example"](./test/.env.example). - ## Plans ### Plans workflow diff --git a/package.json b/package.json index a1fef45c..4905f5df 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "redis-filtered-sort": "^2.3.0", "request": "^2.88.2", "stdout-stream": "^1.4.1", - "stripe": "^8.61.0", "urlsafe-base64": "^1.0.0", "yargs": "^15.3.1" }, diff --git a/schemas/balance/decrement.json b/schemas/balance/decrement.json deleted file mode 100644 index ca927cea..00000000 --- a/schemas/balance/decrement.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$id": "balance.decrement", - "type": "object", - "additionalProperties": false, - "required": [ - "ownerId", - "amount", - "idempotency", - "goal" - ], - "properties": { - "ownerId": { - "type": "string", - "minLength": 1, - "maxLength": 65536 - }, - "amount": { - "type": "integer", - "minimum": 1, - "maximum": 1000000, - "description": "A positive integer representing how much to charge" - }, - "idempotency": { - "type": "string", - "minLength": 1, - "maxLength": 65536, - "description": "An idempotency key" - }, - "goal": { - "type": "string", - "minLength": 1, - "maxLength": 65536 - } - } -} diff --git a/schemas/balance/get.json b/schemas/balance/get.json deleted file mode 100644 index 2046f24e..00000000 --- a/schemas/balance/get.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$id": "balance.get", - "type": "object", - "additionalProperties": false, - "properties": { - "owner": { - "$ref": "common#/definitions/owner" - } - } -} diff --git a/schemas/charge/get.json b/schemas/charge/get.json deleted file mode 100644 index 93ab018d..00000000 --- a/schemas/charge/get.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$id": "charge.get", - "type": "object", - "additionalProperties": false, - "required": [ - "id" - ], - "properties": { - "id": { - "$ref": "common#/definitions/chargeId" - } - } -} diff --git a/schemas/charge/list.json b/schemas/charge/list.json deleted file mode 100644 index b6affce1..00000000 --- a/schemas/charge/list.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$id": "charge.list", - "type": "object", - "additionalProperties": false, - "required": [ - "limit", - "offset" - ], - "properties": { - "owner": { - "$ref": "common#/definitions/owner" - }, - "limit": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 20 - }, - "offset": { - "type": "integer", - "minimum": 0, - "default": 0 - } - } -} diff --git a/schemas/charge/paypal/capture.json b/schemas/charge/paypal/capture.json deleted file mode 100644 index da1159d6..00000000 --- a/schemas/charge/paypal/capture.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$id": "charge.paypal.capture", - "type": "object", - "additionalProperties": false, - "required": [ - "paymentId" - ], - "properties": { - "paymentId": { - "type": "string", - "minLength": 1, - "maxLength": 1024 - } - } -} diff --git a/schemas/charge/paypal/create.json b/schemas/charge/paypal/create.json deleted file mode 100644 index bda372ad..00000000 --- a/schemas/charge/paypal/create.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$id": "charge.paypal.create", - "type": "object", - "additionalProperties": false, - "required": [ - "amount", - "description", - "returnUrl", - "cancelUrl" - ], - "properties": { - "amount": { - "type": "integer", - "minimum": 1, - "maximum": 1000000, - "description": "A positive integer representing how much to charge" - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 65536, - "description": "An arbitrary string which you can attach to a charge object" - }, - "returnUrl": { - "type": "string", - "format": "uri" - }, - "cancelUrl": { - "type": "string", - "format": "uri" - } - } -} diff --git a/schemas/charge/paypal/return.json b/schemas/charge/paypal/return.json deleted file mode 100644 index 3e06406d..00000000 --- a/schemas/charge/paypal/return.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$id": "charge.paypal.return", - "type": "object", - "additionalProperties": false, - "required": [ - "PayerID", - "paymentId" - ], - "properties": { - "PayerID": { - "type": "string", - "minLength": 1, - "maxLength": 1024 - }, - "paymentId": { - "type": "string", - "minLength": 1, - "maxLength": 1024 - }, - "token": { - "type": "string", - "minLength": 1, - "maxLength": 1024, - "description": "Paypal token" - } - } -} diff --git a/schemas/charge/paypal/void.json b/schemas/charge/paypal/void.json deleted file mode 100644 index b74f9041..00000000 --- a/schemas/charge/paypal/void.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$id": "charge.paypal.void", - "type": "object", - "additionalProperties": false, - "required": [ - "paymentId" - ], - "properties": { - "paymentId": { - "type": "string", - "minLength": 1, - "maxLength": 1024 - } - } -} diff --git a/schemas/charge/stripe/create.json b/schemas/charge/stripe/create.json deleted file mode 100644 index 7a1c0333..00000000 --- a/schemas/charge/stripe/create.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$id": "charge.stripe.create", - "type": "object", - "additionalProperties": false, - "required": [ - "amount", - "description" - ], - "if": { - "required": ["saveCard"], - "properties": { - "saveCard": { "const": true } - } - }, - "then": { - "required": ["email", "token"] - }, - "properties": { - "amount": { - "type": "integer", - "minimum": 1, - "maximum": 1000000, - "description": "A positive integer representing how much to charge" - }, - "description": { - "type": "string", - "minLength": 1, - "maxLength": 65536, - "description": "An arbitrary string which you can attach to a charge object" - }, - "statementDescriptor": { - "type": "string", - "minLength": 1, - "maxLength": 22, - "description": "An arbitrary string to be displayed on your customer’s credit card statement" - }, - "saveCard": { - "type": "boolean", - "description": "Save card for a future charges", - "default": false - }, - "email": { - "type": "string", - "format": "email" - }, - "token": { - "type": "string", - "minLength": 1, - "maxLength": 65536, - "description": "Token from stripe Checkout API. Stored card will be used if token is empty" - }, - "metadata": { - "type": "object", - "description": "Set of key-value pairs that you can attach to a charge object", - "default": {} - } - } -} diff --git a/schemas/charge/stripe/webhook.json b/schemas/charge/stripe/webhook.json deleted file mode 100644 index b323b60e..00000000 --- a/schemas/charge/stripe/webhook.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$id": "charge.stripe.webhook", - "type": "object" -} diff --git a/schemas/common.json b/schemas/common.json index e654de77..e4b34242 100644 --- a/schemas/common.json +++ b/schemas/common.json @@ -8,12 +8,6 @@ "title": "Payment owner", "description": "Identification of owner" }, - "chargeId": { - "type": "string", - "format": "uuid", - "title": "Charge id", - "description": "Identification of charge" - }, "currency": { "type": "object", "description": "Generic currency object", diff --git a/schemas/response/balance/decrement.json b/schemas/response/balance/decrement.json deleted file mode 100644 index f6f6956b..00000000 --- a/schemas/response/balance/decrement.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$id": "response.balance.decrement", - "type": "number" -} diff --git a/schemas/response/balance/get.json b/schemas/response/balance/get.json deleted file mode 100644 index 13a5f6b4..00000000 --- a/schemas/response/balance/get.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$id": "response.balance.get", - "title": "balance.get response", - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "description": "Balance information", - "type":"object", - "additionalProperties": false, - "properties": { - "type": { - "type": "string", - "const": "balance" - }, - "id": { - "$ref": "common#/definitions/owner" - }, - "attributes": { - "type": "object", - "additionalProperties": false, - "properties": { - "value": { - "type": "number" - } - } - } - } - } - } -} diff --git a/schemas/response/charge/get.json b/schemas/response/charge/get.json deleted file mode 100644 index 6d70b02b..00000000 --- a/schemas/response/charge/get.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$id": "response.charge.get", - "title": "Charge", - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "description": { - "type": "string" - }, - "status": { - "type": "number" - }, - "createAt": { - "type": "string", - "format": "date" - }, - "owner": { - "$ref": "common#/definitions/owner" - }, - "failReason": { - "type": "string" - } - } - } - } -} diff --git a/schemas/response/charge/list.json b/schemas/response/charge/list.json deleted file mode 100644 index d166fdd0..00000000 --- a/schemas/response/charge/list.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "$id": "response.charge.list", - "title": "List of charges", - "type": "object", - "properties": { - "data": { - "description": "Charges list", - "type": "array", - "items": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "description": { - "type": "string" - }, - "status": { - "type": "number" - }, - "createAt": { - "type": "string", - "format": "date" - }, - "owner": { - "$ref": "common#/definitions/owner" - }, - "failReason": { - "type": "string" - } - } - } - }, - "meta": { - "type": "object", - "properties": { - "offset": { - "type": "number" - }, - "limit": { - "type": "number" - }, - "cursor": { - "type": "number" - }, - "page": { - "type": "number" - }, - "pages": { - "type": "number" - } - } - } - } -} diff --git a/schemas/response/charge/paypal/capture.json b/schemas/response/charge/paypal/capture.json deleted file mode 100644 index 341ac43a..00000000 --- a/schemas/response/charge/paypal/capture.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$id": "response.charge.paypal.capture", - "title": "Paypal charge result", - "type": "object", - "properties": { - "data": { - "description": "Charge info", - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "description": { - "type": "string" - }, - "status": { - "type": "number" - }, - "createAt": { - "type": "string", - "format": "date" - }, - "owner": { - "$ref": "common#/definitions/owner" - }, - "failReason": { - "type": "string" - } - } - } - } -} diff --git a/schemas/response/charge/paypal/create.json b/schemas/response/charge/paypal/create.json deleted file mode 100644 index 0df2652a..00000000 --- a/schemas/response/charge/paypal/create.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "$id": "response.charge.paypal.create", - "title": "Create paypal charge", - "type": "object", - "properties": { - "data": { - "description": "Charge information", - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "description": { - "type": "string" - }, - "status": { - "type": "number" - }, - "createAt": { - "type": "string", - "format": "date" - }, - "owner": { - "$ref": "common#/definitions/owner" - }, - "failReason": { - "type": "string" - } - } - }, - "meta": { - "type": "object", - "properties": { - "paypal": { - "description": "Paypal charge metadata", - "type": "object", - "properties": { - "approvalUrl": { - "$ref": "common#/definitions/links" - }, - "paymentId": { - "$ref": "common#/definitions/paymentId" - } - } - } - } - } - } -} diff --git a/schemas/response/charge/paypal/return.json b/schemas/response/charge/paypal/return.json deleted file mode 100644 index 9d0602f6..00000000 --- a/schemas/response/charge/paypal/return.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "$id": "response.charge.paypal.return", - "title": "Paypal charge return", - "type": "object", - "properties": { - "data": { - "description": "Charge information", - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "const": "charge" - }, - "attributes": { - "type": "object", - "properties": { - "amount": { - "type": "string" - }, - "description": { - "type": "string" - }, - "status": { - "type": "number" - }, - "createAt": { - "type": "string", - "format": "date-time" - }, - "owner": { - "$ref": "common#/definitions/owner" - }, - "failReason": { - "type": "string" - } - } - } - } - }, - "meta": { - "description": "Paypal metadata", - "type": "object", - "properties": { - "paypal": { - "type":"object", - "properties": { - "payer": { - "$ref": "common#/definitions/payer" - } - } - } - } - } - } -} diff --git a/schemas/response/charge/paypal/void.json b/schemas/response/charge/paypal/void.json deleted file mode 100644 index 55875890..00000000 --- a/schemas/response/charge/paypal/void.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$id": "response.charge.paypal.void", - "title": "Void paypal charge", - "type": "object", - "properties": { - "data": { - "description": "Paypal charge", - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "description": { - "type": "string" - }, - "status": { - "type": "number" - }, - "createAt": { - "type": "string", - "format": "date" - }, - "owner": { - "$ref": "common#/definitions/owner" - }, - "failReason": { - "type": "string" - } - } - } - } -} diff --git a/schemas/response/charge/stripe/create.json b/schemas/response/charge/stripe/create.json deleted file mode 100644 index ef4718e6..00000000 --- a/schemas/response/charge/stripe/create.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$id": "response.charge.stripe.create", - "title": "Create stripe charge", - "type": "object", - "properties": { - "data": { - "description": "Stripe charge", - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "description": { - "type": "string" - }, - "status": { - "type": "number" - }, - "createAt": { - "type": "string", - "format": "date" - }, - "owner": { - "$ref": "common#/definitions/owner" - }, - "failReason": { - "type": "string" - } - } - } - } -} diff --git a/schemas/response/charge/stripe/webhook.json b/schemas/response/charge/stripe/webhook.json deleted file mode 100644 index cf17a767..00000000 --- a/schemas/response/charge/stripe/webhook.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$id": "response.charge.stripe.webhook", - "title": "Create stripe webhook", - "type": "object", - "additionalProperties": false, - "properties": { - "received": { - "type": "boolean", - "const": true - } - } -} diff --git a/scripts/decrementBalance.lua b/scripts/decrementBalance.lua deleted file mode 100644 index c5736efc..00000000 --- a/scripts/decrementBalance.lua +++ /dev/null @@ -1,41 +0,0 @@ -local userBalanceKey = KEYS[1] -local userBalanceDecrementIdempotencyKey = KEYS[2] -local userBalanceGoalKey = KEYS[3] - -local amount = ARGV[1] -local idempotency = ARGV[2] -local goal = ARGV[3] - -assert(type(amount) ~= "number", "amount expects a number") - --- Idempotency -local alreadyDecremented = redis.call("HGET", userBalanceDecrementIdempotencyKey, idempotency) - -if alreadyDecremented ~= false then - return redis.error_reply('409') -end - --- balance doesn't exists -if redis.call("EXISTS", userBalanceKey) == false then - return redis.error_reply('409') -end - --- goal -local goalAmount = redis.call("HGET", userBalanceGoalKey, goal) - -if goalAmount ~= amount then - return redis.error_reply('409') -end - --- has money -local balance = redis.call("GET", userBalanceKey) - -if (balance - amount) < 0 then - return redis.error_reply('409') -end - -redis.call("DECRBY", userBalanceKey, amount) -redis.call("HSET", userBalanceDecrementIdempotencyKey, idempotency, amount) -redis.call("HDEL", userBalanceGoalKey, goal) - -return redis.call("GET", userBalanceKey) diff --git a/scripts/incrementBalance.lua b/scripts/incrementBalance.lua deleted file mode 100644 index 5243a07e..00000000 --- a/scripts/incrementBalance.lua +++ /dev/null @@ -1,21 +0,0 @@ -local userBalanceKey = KEYS[1] -local userBalanceIncrementIdempotencyKey = KEYS[2] -local userBalanceGoalKey = KEYS[3] - -local amount = ARGV[1] -local idempotency = ARGV[2] -local goal = ARGV[3] - -assert(type(amount) ~= "number", "amount expects a number") - -local alreadyIncremented = redis.call("HGET", userBalanceIncrementIdempotencyKey, idempotency) - -if alreadyIncremented ~= false then - return redis.error_reply('409') -end - -redis.call("INCRBY", userBalanceKey, amount) -redis.call("HSET", userBalanceIncrementIdempotencyKey, idempotency, amount) -redis.call("HINCRBY", userBalanceGoalKey, goal, amount) - -return redis.call("GET", userBalanceKey) diff --git a/src/actions/balance/decrement.js b/src/actions/balance/decrement.js deleted file mode 100644 index 7d81502f..00000000 --- a/src/actions/balance/decrement.js +++ /dev/null @@ -1,35 +0,0 @@ -const { ActionTransport } = require('@microfleet/core'); -const { LockAcquisitionError } = require('ioredis-lock'); -const { HttpStatusError } = require('common-errors'); -const Promise = require('bluebird'); - -const acquireLock = require('../../utils/acquire-lock'); - -const concurrentRequests = new HttpStatusError(429, 'multiple concurrent requests'); - -/** - * @api {amqp} .balance.decrement Decrement balance - * @apiVersion 1.0.0 - * @apiName balanceDecrement - * @apiGroup Balance - * - * @apiSchema {jsonschema=balance/decrement.json} apiRequest - * @apiSchema {jsonschema=response/balance/decrement.json} apiResponse - */ -async function decrementBalanceAction(service, request) { - const { ownerId, amount, idempotency, goal } = request.params; - - return service.balance.decrement(ownerId, amount, idempotency, goal); -} - -async function wrappedAction(request) { - return Promise - .using(this, request, acquireLock( - this, `tx!balance:decrement:${request.params.owner}:${request.params.idempotency}` - ), decrementBalanceAction) - .catchThrow(LockAcquisitionError, concurrentRequests); -} - -wrappedAction.transports = [ActionTransport.amqp]; - -module.exports = wrappedAction; diff --git a/src/actions/balance/get.js b/src/actions/balance/get.js deleted file mode 100644 index 8824f18c..00000000 --- a/src/actions/balance/get.js +++ /dev/null @@ -1,31 +0,0 @@ -const { ActionTransport } = require('@microfleet/core'); - -const checkAllowedForAdmin = require('../../middlewares/admin-request-owner'); -const { balance: balanceResponse } = require('../../utils/json-api'); - -/** - * @api {get} .balance.get Get balance - * @apiVersion 1.0.0 - * @apiName balanceGet - * @apiGroup Balance - * - * @apiSchema {jsonschema=balance/decrement.json} apiRequest - * @apiSchema {jsonschema=response/balance/decrement.json} apiResponse - */ -async function getBalanceAction(request) { - const { owner } = request.locals; - const balance = await this.balance.get(owner.id); - - return balanceResponse(owner.alias, balance); -} - -getBalanceAction.auth = 'token'; -getBalanceAction.allowed = checkAllowedForAdmin; -getBalanceAction.transports = [ActionTransport.http]; -getBalanceAction.transportOptions = { - [ActionTransport.http]: { - methods: ['get'], - }, -}; - -module.exports = getBalanceAction; diff --git a/src/actions/charge/get.js b/src/actions/charge/get.js deleted file mode 100644 index e7ee0141..00000000 --- a/src/actions/charge/get.js +++ /dev/null @@ -1,48 +0,0 @@ -const { HttpStatusError } = require('common-errors'); -const { ActionTransport } = require('@microfleet/core'); -const { USERS_ADMIN_ROLE } = require('ms-users/lib/constants'); - -const { CHARGE_RESPONSE_FIELDS, charge: chargeResponse } = require('../../utils/json-api'); - -const notAllowedHttpError = new HttpStatusError(403, 'not enough rights'); - -function assertEnouthRights(charge, user) { - const { id: ownerId, roles } = user; - - if (roles.includes(USERS_ADMIN_ROLE) === false && charge.owner !== ownerId) { - throw notAllowedHttpError; - } -} - -/** - * @api {http-get} .charge.get Get charge - * @apiVersion 1.0.0 - * @apiName chargeGet - * @apiGroup Charge - * - * @apiDescription Get the charge information - * - * @apiSchema {jsonschema=charge/get.json} apiRequest - * @apiSchema {jsonschema=response/charge/get.json} apiResponse - */ -async function getChargeAction({ auth, query }) { - const { users: { audience } } = this.config; - const user = auth.credentials.metadata[audience]; - const charge = await this.charge.get(query.id, CHARGE_RESPONSE_FIELDS); - - if (charge !== null) { - assertEnouthRights(charge, user); - } - - return chargeResponse(charge, { owner: '[[protected]]' }); -} - -getChargeAction.auth = 'token'; -getChargeAction.transports = [ActionTransport.http]; -getChargeAction.transportOptions = { - [ActionTransport.http]: { - methods: ['get'], - }, -}; - -module.exports = getChargeAction; diff --git a/src/actions/charge/list.js b/src/actions/charge/list.js deleted file mode 100644 index 4ad04d3d..00000000 --- a/src/actions/charge/list.js +++ /dev/null @@ -1,34 +0,0 @@ -const { ActionTransport } = require('@microfleet/core'); - -const checkAllowedForAdmin = require('../../middlewares/admin-request-owner'); -const { CHARGE_RESPONSE_FIELDS, chargeCollection } = require('../../utils/json-api'); - -/** - * @api {http-get} .charge.list List charges - * @apiVersion 1.0.0 - * @apiName chargeList - * @apiGroup Charge - * - * @apiDescription Get the list of charges - * - * @apiSchema {jsonschema=charge/list.json} apiRequest - * @apiSchema {jsonschema=response/charge/list.json} apiResponse - */ -async function chargesListAction(request) { - const { owner } = request.locals; - const { offset, limit } = request.method === 'amqp' ? request.params : request.query; - const [charges, total] = await this.charge.list(owner.id, offset, limit, CHARGE_RESPONSE_FIELDS); - - return chargeCollection(charges, { owner: owner.alias }, total, limit, offset); -} - -chargesListAction.auth = 'token'; -chargesListAction.allowed = checkAllowedForAdmin; -chargesListAction.transports = [ActionTransport.amqp, ActionTransport.http]; -chargesListAction.transportOptions = { - [ActionTransport.http]: { - methods: ['get'], - }, -}; - -module.exports = chargesListAction; diff --git a/src/actions/charge/paypal/capture.js b/src/actions/charge/paypal/capture.js deleted file mode 100644 index 1ce4e2e5..00000000 --- a/src/actions/charge/paypal/capture.js +++ /dev/null @@ -1,74 +0,0 @@ -const { ActionTransport } = require('@microfleet/core'); -const { LockAcquisitionError } = require('ioredis-lock'); -const { HttpStatusError } = require('common-errors'); -const Promise = require('bluebird'); -const assert = require('assert'); - -const acquireLock = require('../../../utils/acquire-lock'); -const assertStringNotEmpty = require('../../../utils/asserts/string-not-empty'); -const { STATUS_AUTHORIZED, retreiveAuthorizationId } = require('../../../utils/charge'); -const { CHARGE_RESPONSE_FIELDS, charge: chargeResponse } = require('../../../utils/json-api'); - -const concurrentRequests = new HttpStatusError(429, 'multiple concurrent requests'); -const alreadyExecutedError = new HttpStatusError(400, 'already executed'); - -async function paypalCaptureAction(service, request) { - const { paymentId } = request.params; - const chargeId = await service.paypal.getInternalId(paymentId); - - assertStringNotEmpty(chargeId); - - const charge = await service.charge.get(chargeId); - const amount = Number(charge.amount); - const sourceMetadata = JSON.parse(charge.sourceMetadata); - const authorizationId = retreiveAuthorizationId(sourceMetadata); - - assert.equal(charge.status, STATUS_AUTHORIZED, alreadyExecutedError); - - const paypalPayment = await service.paypal.capture(authorizationId, amount); - sourceMetadata.authorization = paypalPayment; - - if (paypalPayment.state.toLowerCase() === 'completed') { - const pipeline = service.redis.pipeline(); - - await service.charge.markAsComplete(chargeId, paymentId, sourceMetadata, pipeline); - await service.balance.increment( - charge.owner, - Number(charge.amount), - paymentId, - authorizationId, // @TODO goal from params - pipeline - ); - await pipeline.exec(); - } else { - await service.charge.markAsFailed(chargeId, paymentId, sourceMetadata, paypalPayment.reason_code); - } - - const updatedCharge = await service.charge.get(chargeId, CHARGE_RESPONSE_FIELDS); - - return chargeResponse(updatedCharge, { owner: updatedCharge.owner }); -} - -/** - * @api {amqp} .charge.paypal.capture Paypal - Capture paypal funds - * @apiVersion 1.0.0 - * @apiName chargePaypalCreate - * @apiGroup Charge.Paypal - * - * @apiDescription Captures requested `charge` - * - * @apiSchema {jsonschema=charge/paypal/capture.json} apiRequest - * @apiSchema {jsonschema=response/charge/paypal/capture.json} apiResponse - */ -async function wrappedAction(request) { - // NOTE: lock is the same as in void so that we - // cant try to void/capture the same payment at the same time - const lockPromise = acquireLock(this, `tx!paypal:complete:${request.params.paymentId}`); - return Promise - .using(this, request, lockPromise, paypalCaptureAction) - .catchThrow(LockAcquisitionError, concurrentRequests); -} - -wrappedAction.transports = [ActionTransport.amqp]; - -module.exports = wrappedAction; diff --git a/src/actions/charge/paypal/create.js b/src/actions/charge/paypal/create.js deleted file mode 100644 index d10e395a..00000000 --- a/src/actions/charge/paypal/create.js +++ /dev/null @@ -1,65 +0,0 @@ -const { ActionTransport } = require('@microfleet/core'); -const { LockAcquisitionError } = require('ioredis-lock'); -const { HttpStatusError } = require('common-errors'); -const Promise = require('bluebird'); - -const { handlePipeline } = require('../../../utils/redis'); -const acquireLock = require('../../../utils/acquire-lock'); -const { CHARGE_SOURCE_PAYPAL } = require('../../../utils/charge'); -const { charge: chargeResponse } = require('../../../utils/json-api'); - -const concurrentRequests = new HttpStatusError(429, 'multiple concurrent requests'); - -async function createPaypalChargeAction(service, request) { - const { id: ownerId } = request.auth.credentials; - const { audience } = service.config.users; - const { alias } = request.auth.credentials.metadata[audience]; - - // use owner id instead of alias - const params = { owner: ownerId, ...request.params }; - const { amount, description, owner, returnUrl, cancelUrl } = params; - - // create internal record - const charge = await service.charge.create(CHARGE_SOURCE_PAYPAL, owner, amount, description, params); - const chargeId = charge.id; - - // create paypal payment - const paypalPayment = await service.paypal - .createPayment(chargeId, { amount, description, returnUrl, cancelUrl }); - const approvalUrl = paypalPayment.links.find((link) => link.rel === 'approval_url'); - const sourceId = paypalPayment.id; - - const pipeline = service.redis.pipeline(); - await service.paypal.setInternalId(sourceId, chargeId, pipeline); - await service.charge.updateSource({ id: chargeId, sourceId, sourceMetadata: paypalPayment }, pipeline); - await pipeline.exec().then(handlePipeline); - - return chargeResponse(charge, { owner: alias }, { paypal: { approvalUrl, paymentId: sourceId } }); -} - -/** - * @api {http-post} .charge.paypal.create Paypal - Create Paypal charge - * @apiVersion 1.0.0 - * @apiName chargePaypalCreate - * @apiGroup Charge.Paypal - * - * @apiSchema {jsonschema=charge/paypal/create.json} apiRequest - * @apiSchema {jsonschema=response/charge/paypal/create.json} apiResponse - */ -async function wrappedAction(request) { - const { id: ownerId } = request.auth.credentials; - - return Promise - .using(this, request, acquireLock(this, `tx!charge:create:paypal:${ownerId}`), createPaypalChargeAction) - .catchThrow(LockAcquisitionError, concurrentRequests); -} - -wrappedAction.auth = 'token'; -wrappedAction.transports = [ActionTransport.amqp, ActionTransport.http]; -wrappedAction.transportOptions = { - [ActionTransport.http]: { - methods: ['post'], - }, -}; - -module.exports = wrappedAction; diff --git a/src/actions/charge/paypal/return.js b/src/actions/charge/paypal/return.js deleted file mode 100644 index 2bdafa07..00000000 --- a/src/actions/charge/paypal/return.js +++ /dev/null @@ -1,66 +0,0 @@ -const { ActionTransport } = require('@microfleet/core'); -const { LockAcquisitionError } = require('ioredis-lock'); -const { HttpStatusError } = require('common-errors'); -const Promise = require('bluebird'); -const assert = require('assert'); - -const acquireLock = require('../../../utils/acquire-lock'); -const assertStringNotEmpty = require('../../../utils/asserts/string-not-empty'); -const { STATUS_INITIALIZED } = require('../../../utils/charge'); -const { CHARGE_RESPONSE_FIELDS, charge: chargeResponse } = require('../../../utils/json-api'); - -const concurrentRequests = new HttpStatusError(429, 'multiple concurrent requests'); -const alreadyExecutedError = new HttpStatusError(400, 'already executed'); - -async function paypalReturnAction(service, request) { - const { paymentId, PayerID: payerId } = request.method === 'amqp' ? request.params : request.query; - const chargeId = await service.paypal.getInternalId(paymentId); - - assertStringNotEmpty(chargeId); - const charge = await service.charge.get(chargeId); - assert.equal(charge.status, STATUS_INITIALIZED, alreadyExecutedError); - - const paypalPayment = await service.paypal.execute(paymentId, payerId); - service.log.info({ paypalPayment }, 'authorized payment'); - - // ensure we have an authorize and not an immediate sale - assert.equal(paypalPayment.intent, 'authorize'); - - const args = [chargeId, paymentId, paypalPayment]; - if (paypalPayment.state.toLowerCase() === 'approved') { - await service.charge.markAsAuthorized(...args); - } else { - await service.charge.markAsFailed(...args, paypalPayment.reason_code); - } - - const updatedCharge = await service.charge.get(chargeId, CHARGE_RESPONSE_FIELDS); - return chargeResponse(updatedCharge, { owner: updatedCharge.owner }, { paypal: { payer: paypalPayment.payer } }); -} - -/** - * @api {http-get} .charge.paypal.return Paypal - Return Paypal funds - * @apiVersion 1.0.0 - * @apiName chargePaypalReturn - * @apiGroup Charge.Paypal - * - * @apiDescription Returns funds - * - * @apiSchema {jsonschema=charge/paypal/return.json} apiRequest - * @apiSchema {jsonschema=response/charge/paypal/return.json} apiResponse - */ -async function wrappedAction(request) { - return Promise - .using(this, request, acquireLock( - this, `tx!paypal:return:${request.query.paymentId}` - ), paypalReturnAction) - .catchThrow(LockAcquisitionError, concurrentRequests); -} - -wrappedAction.transports = [ActionTransport.amqp, ActionTransport.http]; -wrappedAction.transportOptions = { - [ActionTransport.http]: { - methods: ['get'], - }, -}; - -module.exports = wrappedAction; diff --git a/src/actions/charge/paypal/void.js b/src/actions/charge/paypal/void.js deleted file mode 100644 index 3e43f5b7..00000000 --- a/src/actions/charge/paypal/void.js +++ /dev/null @@ -1,60 +0,0 @@ -const { ActionTransport } = require('@microfleet/core'); -const { LockAcquisitionError } = require('ioredis-lock'); -const { HttpStatusError } = require('common-errors'); -const Promise = require('bluebird'); -const assert = require('assert'); - -const acquireLock = require('../../../utils/acquire-lock'); -const assertStringNotEmpty = require('../../../utils/asserts/string-not-empty'); -const { STATUS_AUTHORIZED, retreiveAuthorizationId } = require('../../../utils/charge'); -const { CHARGE_RESPONSE_FIELDS, charge: chargeResponse } = require('../../../utils/json-api'); - -const concurrentRequests = new HttpStatusError(429, 'multiple concurrent requests'); -const alreadyExecutedError = new HttpStatusError(400, 'already executed'); - -async function paypalVoidAction(service, request) { - const { paymentId } = request.params; - const chargeId = await service.paypal.getInternalId(paymentId); - - assertStringNotEmpty(chargeId); - - const charge = await service.charge.get(chargeId); - assert.equal(charge.status, STATUS_AUTHORIZED, alreadyExecutedError); - const sourceMetadata = JSON.parse(charge.sourceMetadata); - const authorizationId = retreiveAuthorizationId(sourceMetadata); - const paypalPayment = await service.paypal.void(authorizationId); - - service.log.info({ paypalPayment }, 'voided paypal payment'); - - sourceMetadata.authorization = paypalPayment; - const action = paypalPayment.state.toLowerCase() === 'voided' - ? 'markAsCanceled' - : 'markAsFailed'; - - await service.charge[action](chargeId, paymentId, sourceMetadata, paypalPayment.reason_code || 'voided'); - - const updatedCharge = await service.charge.get(chargeId, CHARGE_RESPONSE_FIELDS); - return chargeResponse(updatedCharge, { owner: updatedCharge.owner }); -} - -/** - * @api {amqp} .charge.paypal.void Paypal - Void paypal charge - * @apiVersion 1.0.0 - * @apiName chargePaypalVoid - * @apiGroup Charge.Paypal - * - * @apiDescription Invalidate `charge` - * - * @apiSchema {jsonschema=charge/paypal/void.json} apiRequest - * @apiSchema {jsonschema=response/charge/paypal/void.json} apiResponse - */ -async function wrappedAction(request) { - const lockPromise = acquireLock(this, `tx!paypal:complete:${request.params.paymentId}`); - return Promise - .using(this, request, lockPromise, paypalVoidAction) - .catchThrow(LockAcquisitionError, concurrentRequests); -} - -wrappedAction.transports = [ActionTransport.amqp]; - -module.exports = wrappedAction; diff --git a/src/actions/charge/stripe/create.js b/src/actions/charge/stripe/create.js deleted file mode 100644 index 538387bc..00000000 --- a/src/actions/charge/stripe/create.js +++ /dev/null @@ -1,99 +0,0 @@ -const { strictEqual } = require('assert'); -const { ActionTransport } = require('@microfleet/core'); -const { LockAcquisitionError } = require('ioredis-lock'); -const { HttpStatusError } = require('common-errors'); -const Promise = require('bluebird'); -const omit = require('lodash/omit'); - -const acquireLock = require('../../../utils/acquire-lock'); -const assertStringNotEmpty = require('../../../utils/asserts/string-not-empty'); -const { CHARGE_SOURCE_STRIPE } = require('../../../utils/charge'); -const { charge: chargeResponse } = require('../../../utils/json-api'); - -const notEnabled = new HttpStatusError(501, 'Stripe is not enabled'); -const concurrentRequests = new HttpStatusError(429, 'multiple concurrent requests'); -const tokenIsRequired = new HttpStatusError(400, 'Stripe token is required'); - -async function selectChargeSource(service, params) { - const { owner, token, saveCard, email } = params; - const storedCustomer = await service.stripe.storedCustomer(owner); - - // stripe token is required if customer not found - if (storedCustomer === null) { - assertStringNotEmpty(token, 'token', tokenIsRequired); - } - - if (saveCard === false) { - return storedCustomer === null ? { source: token } : { customer: storedCustomer.id }; - } - - const customer = storedCustomer === null - ? await service.stripe.createCustomer(owner, { source: token, email }) - : await service.stripe.updateCustomer(owner, storedCustomer.id, { source: token, email }); - - return { customer: customer.id }; -} - -async function createStripeCharge(service, charge, source, params) { - const { amount, description, statementDescriptor, metadata, email } = params; - const stripeChargeParams = { amount, description, statementDescriptor, metadata }; - - if (email !== undefined) { - stripeChargeParams.receipt_email = email; - } - - try { - await service.stripe.charge(charge.id, { ...source, ...stripeChargeParams }); - } catch (error) { - service.log.error(`Stripe charge for ${charge.owner} is failed`, error, charge); - } -} - -/** - * @api {http-post} .charge.stripe.create Stripe - Create charge - * @apiVersion 1.0.0 - * @apiName chargeStripeCreate - * @apiGroup Charge.Stripe - * - * @apiDescription Creates new Stripe charge - * - * @apiSchema {jsonschema=charge/stripe/create.json} apiRequest - * @apiSchema {jsonschema=response/charge/stripe/create.json} apiResponse - */ -async function createStripeChargeAction(service, request) { - strictEqual(service.config.stripe.enabled, true, notEnabled); - - const { id: ownerId } = request.auth.credentials; - const { audience } = service.config.users; - const { alias } = request.auth.credentials.metadata[audience]; - // use owner id instead of alias - const params = { owner: ownerId, ...request.params }; - const { owner, amount, description } = params; - // next method should call first because it's validate source - const stripeChargeSource = await selectChargeSource(service, params); - // next method create internal record about charge - const charge = await service.charge.create(CHARGE_SOURCE_STRIPE, owner, amount, description, omit(params, ['token'])); - - // note it's not throw any errors - await createStripeCharge(service, charge, stripeChargeSource, params); - - return chargeResponse(charge, { owner: alias }); -} - -async function wrappedAction(request) { - const { id: ownerId } = request.auth.credentials; - - return Promise - .using(this, request, acquireLock(this, `tx!charge:create:stripe:${ownerId}`), createStripeChargeAction) - .catchThrow(LockAcquisitionError, concurrentRequests); -} - -wrappedAction.auth = 'token'; -wrappedAction.transports = [ActionTransport.http]; -wrappedAction.transportOptions = { - [ActionTransport.http]: { - methods: ['post'], - }, -}; - -module.exports = wrappedAction; diff --git a/src/actions/charge/stripe/webhook.js b/src/actions/charge/stripe/webhook.js deleted file mode 100644 index ed4b5ec0..00000000 --- a/src/actions/charge/stripe/webhook.js +++ /dev/null @@ -1,54 +0,0 @@ -const { ActionTransport } = require('@microfleet/core'); - -/** - * @api {http-post} .charge.stripe.webhook Stripe - Webhook handler - * @apiVersion 1.0.0 - * @apiName chargeStripeWebhook - * @apiGroup Charge.Stripe - * - * @apiDescription Handles requests from Stripe - * - * @apiSchema {jsonschema=charge/stripe/webhook.json} apiRequest - * @apiSchema {jsonschema=response/charge/stripe/webhook.json} apiResponse - */ -async function stripeWebhookAction(request) { - // @todo check IP from white list (it's already in config) - const event = await this.stripe.getEventFromRequest('charge', request); - - if (event.type === 'charge.succeeded') { - const { id: sourceId, metadata: { internalId } } = event.data.object; - const { amount, owner } = await this.charge.get(internalId); - const pipeline = this.redis.pipeline(); - - await this.charge.markAsComplete(internalId, sourceId, event, pipeline); - await this.balance.increment(owner, Number(amount), internalId, internalId, pipeline); - await pipeline.exec(); - } - - if (event.type === 'charge.failed') { - const { id: sourceId, metadata: { internalId }, failure_message: errorMessage } = event.data.object; - await this.charge.markAsFailed(internalId, sourceId, event, errorMessage); - } - - return { received: true }; -} - -stripeWebhookAction.transports = [ActionTransport.http]; -stripeWebhookAction.transportOptions = { - [ActionTransport.http]: { - methods: ['post'], - }, - handlers: { - hapi: { - method: ['POST'], - options: { - payload: { - output: 'data', - parse: false, - }, - }, - }, - }, -}; - -module.exports = stripeWebhookAction; diff --git a/src/config/charge.js b/src/config/charge.js deleted file mode 100644 index 4bc2e2b8..00000000 --- a/src/config/charge.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - charge: { - paypal: { - client: { - mode: 'sandbox', - client_id: 'AdwVgBbIvVaPnlauY91S1-ifPMiQ1R2ZFiq7O6biwc60lcJTpdq9O_o-aFSfHTH9Bt2ly34s1lrQ-Dod', - client_secret: 'EKO6YQ7VC_56ero33GRm8pz9ZYXGX2uPc6E8QxV7FgiJVq3t_EmPdthONsjN_jRj0Cbi8lYQxv9leZXk', - }, - }, - }, -}; diff --git a/src/config/stripe.js b/src/config/stripe.js deleted file mode 100644 index ca88530c..00000000 --- a/src/config/stripe.js +++ /dev/null @@ -1,42 +0,0 @@ -module.exports = { - stripe: { - enabled: false, - secretKey: null, - publicKey: null, - client: { - retry: { - interval: 500, - backoff: 500, - max_interval: 5000, - timeout: 5000, - max_tries: 10, - throw_original: true, - predicate: { code: 429 }, - }, - apiVersion: '2019-05-16', - }, - webhook: { - enabled: false, - trustedIPs: [ - '54.187.174.169', - '54.187.205.235', - '54.187.216.72', - '54.241.31.99', - '54.241.31.102', - '54.241.34.107', - ], - endpoints: [ - { - id: 'charge', - forceRecreate: true, - url: 'https://payments/charge/stripe/webhook', - enabledEvents: [ - 'charge.failed', - 'charge.succeeded', - ], - apiVersion: '2019-05-16', - }, - ], - }, - }, -}; diff --git a/src/payments.js b/src/payments.js index 42902b37..e88439cc 100755 --- a/src/payments.js +++ b/src/payments.js @@ -11,10 +11,6 @@ const conf = require('./conf'); const createPlan = require('./actions/plan/create'); const syncSaleTransactions = require('./actions/sale/sync'); const syncAgreements = require('./actions/agreement/sync'); -const Balance = require('./utils/balance'); -const Charge = require('./utils/charge'); -const Stripe = require('./utils/stripe'); -const Paypal = require('./utils/paypal-payment'); /** * Class representing payments handling @@ -43,24 +39,6 @@ class Payments extends Microfleet { )); } - this.addConnector(ConnectorsTypes.application, async () => { - if (this.config.stripe.enabled === true) { - this.stripe = new Stripe(this.config.stripe, this.redis); - - if (this.config.stripe.webhook.enabled === true) { - if (process.env.NODE_ENV === 'test') { - await this.stripe.dropHooks(); - } - - await this.stripe.setupWebhook(); - } - } - - this.balance = new Balance(this.redis); - this.charge = new Charge(this.redis); - this.paypal = new Paypal({ urls: this.config.urls, ...this.config.charge.paypal }, this.redis); - }); - // init plans and sync transactions during startup of production // service if (process.env.NODE_ENV === 'production') { diff --git a/src/utils/asserts/array.js b/src/utils/asserts/array.js deleted file mode 100644 index a3540bc2..00000000 --- a/src/utils/asserts/array.js +++ /dev/null @@ -1,5 +0,0 @@ -const { strictEqual } = require('assert'); - -module.exports = function assertArray(value, error) { - strictEqual(Array.isArray(value), true, error); -}; diff --git a/src/utils/asserts/integer.js b/src/utils/asserts/integer.js deleted file mode 100644 index c6fafc62..00000000 --- a/src/utils/asserts/integer.js +++ /dev/null @@ -1,6 +0,0 @@ -const { strictEqual } = require('assert'); -const isInteger = require('lodash/isInteger'); - -module.exports = function assertInteger(value, error) { - strictEqual(isInteger(value), true, error); -}; diff --git a/src/utils/asserts/plain-object.js b/src/utils/asserts/plain-object.js deleted file mode 100644 index 621824b5..00000000 --- a/src/utils/asserts/plain-object.js +++ /dev/null @@ -1,6 +0,0 @@ -const { strictEqual } = require('assert'); -const isPlainObject = require('lodash/isPlainObject'); - -module.exports = function assertPlainObject(value, error) { - strictEqual(isPlainObject(value), true, error); -}; diff --git a/src/utils/asserts/string-not-empty.js b/src/utils/asserts/string-not-empty.js deleted file mode 100644 index 4750d83a..00000000 --- a/src/utils/asserts/string-not-empty.js +++ /dev/null @@ -1,7 +0,0 @@ -const { strictEqual } = require('assert'); -const isString = require('lodash/isString'); - -module.exports = function assertStringNotEmpty(value, error) { - strictEqual(isString(value), true, error); - strictEqual(value.length !== 0, true, error); -}; diff --git a/src/utils/asserts/string.js b/src/utils/asserts/string.js deleted file mode 100644 index f76da9b5..00000000 --- a/src/utils/asserts/string.js +++ /dev/null @@ -1,6 +0,0 @@ -const { strictEqual } = require('assert'); -const isString = require('lodash/isString'); - -module.exports = function assertString(value, error) { - strictEqual(isString(value), true, error); -}; diff --git a/src/utils/balance.js b/src/utils/balance.js deleted file mode 100644 index b564283a..00000000 --- a/src/utils/balance.js +++ /dev/null @@ -1,118 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const isObject = require('lodash/isObject'); -const { strictEqual } = require('assert'); - -const assertStringNotEmpty = require('./asserts/string-not-empty'); -const assertInteger = require('./asserts/integer'); - -const incrementScript = fs.readFileSync(path.resolve(__dirname, '../../scripts/incrementBalance.lua'), 'utf8'); -const decrementScript = fs.readFileSync(path.resolve(__dirname, '../../scripts/decrementBalance.lua'), 'utf8'); - -class Balance { - static userBalanceKey(owner) { - assertStringNotEmpty(owner, 'owner is invalid'); - - return `${owner}:balance`; - } - - static userBalanceIncrementIdempotencyKey(owner) { - assertStringNotEmpty(owner, 'owner is invalid'); - - return `${owner}:balance:increment:idempotency`; - } - - static userBalanceDecrementIdempotencyKey(owner) { - assertStringNotEmpty(owner, 'owner is invalid'); - - return `${owner}:balance:decrement:idempotency`; - } - - static userBalanceGoalKey(owner) { - assertStringNotEmpty(owner, 'owner is invalid'); - - return `${owner}:balance:goal`; - } - - static castToNumber(value) { - const balance = Number(value); - - assertInteger(balance, 'balance is invalid'); - - return balance; - } - - constructor(redis) { - strictEqual(isObject(redis), true, 'redis is invalid'); - - this.redis = redis; - } - - async get(owner) { - const value = await this.redis.get(Balance.userBalanceKey(owner)); - - if (value !== null) { - return Balance.castToNumber(value); - } - - return 0; - } - - async increment(owner, amount, idempotency, goal, pipeline) { - assertStringNotEmpty(owner, 'owner is invalid'); - assertInteger(amount, 'amount is invalid'); - assertStringNotEmpty(idempotency, 'idempotency is invalid'); - assertStringNotEmpty(goal, 'goal is invalid'); - - const params = [ - 3, - Balance.userBalanceKey(owner), - Balance.userBalanceIncrementIdempotencyKey(owner), - Balance.userBalanceGoalKey(owner), - amount, - idempotency, - goal, - ]; - - if (pipeline !== undefined) { - strictEqual(isObject(pipeline), true, 'redis pipeline is invalid'); - - // https://github.com/luin/ioredis/issues/536 - return pipeline.eval(incrementScript, ...params); - } - - const value = await this.redis.incrementBalance(...params); - - return Balance.castToNumber(value); - } - - async decrement(owner, amount, idempotency, goal, pipeline) { - assertStringNotEmpty(owner, 'owner is invalid'); - assertInteger(amount, 'amount is invalid'); - assertStringNotEmpty(idempotency, 'idempotency is invalid'); - assertStringNotEmpty(goal, 'goal is invalid'); - - const params = [ - 3, - Balance.userBalanceKey(owner), - Balance.userBalanceDecrementIdempotencyKey(owner), - Balance.userBalanceGoalKey(owner), - amount, - idempotency, - goal, - ]; - - if (pipeline !== undefined) { - strictEqual(isObject(pipeline), true, 'redis pipeline is invalid'); - - // https://github.com/luin/ioredis/issues/536 - return pipeline.eval(decrementScript, ...params); - } - - const value = await this.redis.decrementBalance(...params); - - return Balance.castToNumber(value); - } -} - -module.exports = Balance; diff --git a/src/utils/charge.js b/src/utils/charge.js deleted file mode 100644 index b4b410fa..00000000 --- a/src/utils/charge.js +++ /dev/null @@ -1,210 +0,0 @@ -const { strictEqual } = require('assert'); -const uuid = require('uuid/v4'); -const moment = require('moment'); -const isObject = require('lodash/isObject'); -const isNull = require('lodash/isNull'); -const zipObject = require('lodash/zipObject'); -const fsort = require('redis-filtered-sort'); - -const assertStringNotEmpty = require('./asserts/string-not-empty'); -const assertInteger = require('./asserts/integer'); -const assertPlainObject = require('./asserts/plain-object'); -const RedisMapper = require('./redis-mapper'); - -class Charge { - static listRedisKey(owner) { - assertStringNotEmpty(owner, 'owner is invalid'); - - return `${owner}:charges`; - } - - static dataRedisKey(chargeId) { - assertStringNotEmpty(chargeId, 'chargeId is invalid'); - - return `charge:${chargeId}`; - } - - constructor(redis) { - strictEqual(isObject(redis), true, 'redis is invalid'); - - this.redis = redis; - this.mapper = new RedisMapper(redis); - } - - async create(source, owner, amount, description = '', meta = {}) { - assertStringNotEmpty(source, 'source is invalid'); - assertStringNotEmpty(owner, 'owner is invalid'); - assertInteger(amount, 'amount is invalid'); - assertStringNotEmpty(description, 'description is invalid'); - assertPlainObject(meta, 'meta is invalid'); - - const pipeline = this.redis.pipeline(); - const id = uuid(); - const charge = { - id, - source, - owner, - amount, - description, - createAt: moment().format(), - createAtTimestamp: Date.now(), - metadata: JSON.stringify(meta), - status: Charge.STATUS_INITIALIZED, - sourceId: '', - sourceMetadata: '', - failReason: '', - }; - - pipeline.sadd(Charge.listRedisKey(owner), id); - pipeline.hmset(Charge.dataRedisKey(id), charge); - - await pipeline.exec(); - - return charge; - } - - async updateSource({ id, sourceId, sourceMetadata, ...rest }, pipeline) { - assertStringNotEmpty(id, 'charge id is invalid'); - assertStringNotEmpty(sourceId, 'sourceId is invalid'); - assertPlainObject(sourceMetadata, 'sourceMetadata is invalid'); - - const chargeUpdateData = { - sourceId, - sourceMetadata: JSON.stringify(sourceMetadata), - ...rest, - }; - - if (pipeline !== undefined) { - pipeline.hmset(Charge.dataRedisKey(id), chargeUpdateData); - } else { - await this.redis.hmset(Charge.dataRedisKey(id), chargeUpdateData); - } - } - - async markAsAuthorized(id, sourceId, sourceMetadata, pipeline) { - const opts = { - id, - sourceId, - sourceMetadata, - status: Charge.STATUS_AUTHORIZED, - }; - - await this.updateSource(opts, pipeline); - } - - async markAsComplete(id, sourceId, sourceMetadata, pipeline) { - const opts = { - id, - sourceId, - sourceMetadata, - status: Charge.STATUS_COMPLETED, - }; - - await this.updateSource(opts, pipeline); - } - - async markAsFailed(id, sourceId, sourceMetadata, failReason) { - assertStringNotEmpty(failReason, 'failReason is invalid'); - - const opts = { - id, - sourceId, - failReason, - sourceMetadata, - status: Charge.STATUS_FAILED, - }; - - await this.updateSource(opts); - } - - async markAsCanceled(id, sourceId, sourceMetadata, failReason) { - assertStringNotEmpty(failReason, 'failReason is invalid'); - - const opts = { - id, - sourceId, - failReason, - sourceMetadata, - status: Charge.STATUS_CANCELED, - }; - - await this.updateSource(opts); - } - - // NOTE: how to `fields` works - // NOTE: does not safe for pagination, could return less results than expected - async list(owner, offset, limit, fields = []) { - assertStringNotEmpty(owner, 'owner is invalid'); - - const result = await this.redis.fsort( - Charge.listRedisKey(owner), - Charge.dataRedisKey('*'), - 'createAtTimestamp', - 'DESC', - fsort.filter({}), - Date.now(), - offset, - limit - ); - - if (result.length < 2) { - return [[], 0]; - } - - const pipeline = this.redis.pipeline(); - const total = Number(result.pop()); - - for (const chargeId of result) { - if (fields.length > 1) { - pipeline.hmget(Charge.dataRedisKey(chargeId), fields); - } else { - pipeline.hgetall(Charge.dataRedisKey(chargeId)); - } - } - - const data = await pipeline.exec(); - const charges = []; - - for (const [, chargeData] of data) { - // eslint-disable-next-line no-nested-ternary - const charge = fields.length > 1 - // I hope there is a more simple way to detect not found (and it is not lua script) - ? (chargeData.every(isNull) ? null : zipObject(fields, chargeData)) - : (Object.keys(chargeData).length !== 0 ? chargeData : null); - - if (charge !== null) { - charges.push(charge); - } - } - - return [charges, total]; - } - - async get(id, fields = []) { - const charge = await this.mapper.get(Charge.dataRedisKey(id), fields); - - if (charge.status !== undefined) { - charge.status = parseInt(charge.status, 10); - } - - return charge; - } - - static retreiveAuthorizationId(paypalPayment) { - const [transaction] = paypalPayment.transactions; - const [relatedResource] = transaction.related_resources; - const { authorization } = relatedResource; - return authorization.id; - } -} - -Charge.STATUS_INITIALIZED = 0; -Charge.STATUS_AUTHORIZED = 4; -Charge.STATUS_FAILED = 1; -Charge.STATUS_CANCELED = 2; -Charge.STATUS_COMPLETED = 3; - -Charge.CHARGE_SOURCE_STRIPE = 'stripe'; -Charge.CHARGE_SOURCE_PAYPAL = 'paypal'; - -module.exports = Charge; diff --git a/src/utils/json-api.js b/src/utils/json-api.js deleted file mode 100644 index 822cf6e5..00000000 --- a/src/utils/json-api.js +++ /dev/null @@ -1,69 +0,0 @@ -const pick = require('lodash/pick'); -const omit = require('lodash/omit'); - -const assertStringNotEmpty = require('./asserts/string-not-empty'); - -const CHARGE_RESPONSE_FIELDS = ['id', 'amount', 'description', 'status', 'createAt', 'owner', 'failReason']; - -function balance(id, value) { - return { - data: { - type: 'balance', - id, - attributes: { - value, - }, - }, - }; -} - -function chargeMapper(data, replacement) { - assertStringNotEmpty(replacement.owner, 'replacement for owner is required'); - - if (data === null) { - return data; - } - - const attributes = omit(pick(data, CHARGE_RESPONSE_FIELDS), ['id']); - - return { - id: data.id, - type: 'charge', - attributes: Object.assign(attributes, replacement), - }; -} - -function chargeCollectionMapper(data) { - return chargeMapper(data, this); -} - -function charge(data, replacement, meta = {}) { - const response = { - data: chargeMapper(data, replacement), - }; - - if (Object.keys(meta).length > 0) { - response.meta = meta; - } - - return response; -} - -function chargeCollection(data, replacement, total, limit, offset) { - return { - meta: { - offset, - limit, - cursor: offset + limit, - page: Math.floor(offset / limit) + 1, - pages: Math.ceil(total / limit) }, - data: data.map(chargeCollectionMapper, replacement), - }; -} - -module.exports = { - CHARGE_RESPONSE_FIELDS, - balance, - charge, - chargeCollection, -}; diff --git a/src/utils/paypal-payment.js b/src/utils/paypal-payment.js deleted file mode 100644 index 85094a3d..00000000 --- a/src/utils/paypal-payment.js +++ /dev/null @@ -1,124 +0,0 @@ -const { ValidationError } = require('common-errors'); -const Promise = require('bluebird'); -const invoke = require('lodash/invoke'); -const paypalClient = require('paypal-rest-sdk'); -const retry = require('bluebird-retry'); - -const assertArray = require('./asserts/array'); -const assertStringNotEmpty = require('./asserts/string-not-empty'); -const assertPlainObject = require('./asserts/plain-object'); -const assertInteger = require('./asserts/integer'); - -const invalidConfig = new ValidationError('Paypal config is invalid'); -// @todo from config -const retryConfig = { - interval: 500, - backoff: 500, - max_interval: 5000, - timeout: 5000, - max_tries: 10, - throw_original: true, - predicate: { code: 429 } }; - -function payloadAmount(amount) { - return { - // cents to dollars - total: (amount / 100).toFixed(2), - currency: 'USD', - }; -} - -class Paypal { - static paymentIdToChargeIdKey() { - return 'paypal-payment:internal:ids'; - } - - constructor(config, redis) { - assertPlainObject(config, invalidConfig); - assertPlainObject(config.client, invalidConfig); - assertPlainObject(config.urls, invalidConfig); - assertStringNotEmpty(config.client.mode, invalidConfig); - assertStringNotEmpty(config.client.client_id, invalidConfig); - assertStringNotEmpty(config.client.client_secret, invalidConfig); - - this.config = config; - this.redis = redis; - } - - async request(path, params) { - assertStringNotEmpty(path, 'invalid path'); - assertArray(params, 'invalid params'); - - return Promise.fromCallback((callback) => retry( - invoke, - { args: [paypalClient, path, ...params, this.config.client, callback], ...retryConfig } - )); - } - - async createPayment(internalId, params) { - assertStringNotEmpty(internalId, 'internalId is invalid'); - assertPlainObject(params, 'params is invalid'); - assertStringNotEmpty(params.description, 'params.description is invalid'); - assertStringNotEmpty(params.returnUrl, 'params.returnUrl is invalid'); - assertStringNotEmpty(params.cancelUrl, 'params.cancelUrl is invalid'); - - const payload = { - intent: 'authorize', - payer: { payment_method: 'paypal' }, - redirect_urls: { - return_url: params.returnUrl, - cancel_url: params.cancelUrl, - }, - transactions: [{ - amount: payloadAmount(params.amount), - description: params.description, - custom: internalId, - }], - }; - - return this.request('payment.create', [payload]); - } - - async execute(paymentId, payerId) { - assertStringNotEmpty(paymentId, 'paymentId is invalid'); - assertStringNotEmpty(payerId, 'payerId is invalid'); - - const payload = { - payer_id: payerId, - }; - - return this.request('payment.execute', [paymentId, payload]); - } - - async capture(paymentId, amount) { - assertStringNotEmpty(paymentId, 'paymentId is invalid'); - assertInteger(amount, 'amount is invalid'); - - const payload = { - amount: payloadAmount(amount), - is_final_capture: true, - }; - - return this.request('authorization.capture', [paymentId, payload]); - } - - async void(paymentId) { - assertStringNotEmpty(paymentId, 'paymentId is invalid'); - - return this.request('authorization.void', [paymentId]); - } - - async setInternalId(paypalPaymentId, internalId, pipeline) { - if (pipeline !== undefined) { - pipeline.hset(Paypal.paymentIdToChargeIdKey(), paypalPaymentId, internalId); - } else { - await this.redis.hset(Paypal.paymentIdToChargeIdKey(), paypalPaymentId, internalId); - } - } - - async getInternalId(paypalPaymentId) { - return this.redis.hget(Paypal.paymentIdToChargeIdKey(), paypalPaymentId); - } -} - -module.exports = Paypal; diff --git a/src/utils/redis-mapper.js b/src/utils/redis-mapper.js deleted file mode 100644 index 4559685e..00000000 --- a/src/utils/redis-mapper.js +++ /dev/null @@ -1,28 +0,0 @@ -const zipObject = require('lodash/zipObject'); -const isNull = require('lodash/isNull'); - -const assertStringNotEmpty = require('./asserts/string-not-empty'); -const assertArray = require('./asserts/array'); - -class RedisMapper { - constructor(redis) { - this.redis = redis; - } - - async get(key, fields = []) { - assertStringNotEmpty(key, 'key is invalid'); - assertArray(fields, 'fields is invalid'); - - if (fields.length === 0) { - const data = await this.redis.hgetall(key); - - return Object.keys(data).length !== 0 ? data : null; - } - - const data = await this.redis.hmget(key, fields); - - return data.every(isNull) ? null : zipObject(fields, data); - } -} - -module.exports = RedisMapper; diff --git a/src/utils/stripe.js b/src/utils/stripe.js deleted file mode 100644 index fe1a7abf..00000000 --- a/src/utils/stripe.js +++ /dev/null @@ -1,207 +0,0 @@ -const { ValidationError } = require('common-errors'); -const Promise = require('bluebird'); -const assert = require('assert'); -const pick = require('lodash/pick'); -const invoke = require('lodash/invoke'); -const isEqual = require('lodash/isEqual'); -const sortBy = require('lodash/sortBy'); -const noop = require('lodash/noop'); -const StripeClient = require('stripe'); -const moment = require('moment'); -const retry = require('bluebird-retry'); - -const assertStringNotEmpty = require('./asserts/string-not-empty'); -const assertString = require('./asserts/string'); -const assertPlainObject = require('./asserts/plain-object'); -const assertArray = require('./asserts/array'); -const RedisMapper = require('./redis-mapper'); - -const invalidConfig = new ValidationError('Stripe config is invalid'); - -class Stripe { - static customerRedisKey(owner) { - assertStringNotEmpty(owner, 'owner is invalid'); - - return `${owner}:stripe:customer`; - } - - static webhookRedisKey(id) { - assertStringNotEmpty(id, 'id is invalid'); - - return `stripe:webhook:${id}`; - } - - constructor(config, redis) { - assertPlainObject(config, invalidConfig); - assertStringNotEmpty(config.secretKey, invalidConfig); - assertStringNotEmpty(config.publicKey, invalidConfig); - assertPlainObject(config.client, invalidConfig); - assertPlainObject(config.client.retry, invalidConfig); - assertStringNotEmpty(config.client.apiVersion, invalidConfig); - - this.client = StripeClient(config.secretKey); - this.livemode = config.secretKey.startsWith('sk_live_'); - this.client.setApiVersion(config.client.apiVersion); - this.config = config; - this.redis = redis; - this.mapper = new RedisMapper(redis); - } - - async request(path, params, idempotencyKey = '') { - assertStringNotEmpty(path, 'invalid path'); - assertArray(params, 'invalid params'); - assertString(idempotencyKey, 'invalid idempotencyKey'); - - const { client: { retry: retryConfig } } = this.config; - const args = [this.client, path, ...params]; - - if (idempotencyKey.length > 0) { - args.push({ idempotency_key: idempotencyKey }); - } - - return retry(invoke, { args, ...retryConfig }); - } - - async storedCustomer(owner) { - return this.mapper.get(Stripe.customerRedisKey(owner)); - } - - async createCustomer(owner, params) { - assertStringNotEmpty(owner, 'owner is invalid'); - assertPlainObject(params, 'params is invalid'); - - const customer = await this.request('customers.create', [params], `customer:create:${owner}`); - const data = pick(customer, Stripe.CUSTOMER_FIELDS_FOR_SAVE); - - data.createAt = moment().format(); - data.owner = owner; - data.metadata = JSON.stringify(customer); - - await this.redis.hmset(Stripe.customerRedisKey(owner), data); - - return data; - } - - async updateCustomer(owner, customerId, params) { - assertStringNotEmpty(owner, 'owner is invalid'); - assertStringNotEmpty(customerId, 'customerId is invalid'); - assertPlainObject(params, 'params is invalid'); - - const updatedCustomer = await this.request('customers.update', [customerId, params]); - const data = pick(updatedCustomer, Stripe.CUSTOMER_FIELDS_FOR_SAVE); - const pipeline = this.redis.pipeline(); - - data.createAt = moment().format(); - data.owner = owner; - data.metadata = JSON.stringify(updatedCustomer); - - pipeline.del(Stripe.customerRedisKey(owner)); - pipeline.hmset(Stripe.customerRedisKey(owner), data); - - await pipeline.exec(); - - return data; - } - - async charge(internalId, params) { - assertStringNotEmpty(internalId, 'internalId is invalid'); - assertPlainObject(params, 'params is invalid'); - - const metadata = params.metadata === undefined - ? { internalId } - : ({ ...params.metadata, internalId }); - const chargeParams = { currency: 'USD', ...params, metadata }; - - return this.request('charges.create', [chargeParams], `charge:${internalId}`); - } - - async dropHooks() { - assert(this.livemode === false, 'must not drop hooks in live mode'); - - const webhooks = await this.request('webhookEndpoints.list', [{ - limit: 100, - }]); - - const work = []; - for (const webhook of webhooks.data) { - work.push(this.request('webhookEndpoints.del', [webhook.id])); - } - - await Promise.all(work); - } - - async setupWebhook() { - for (const webhookConfig of this.config.webhook.endpoints) { - const { id, forceRecreate, url, enabledEvents } = webhookConfig; - let webhook = null; - - // eslint-disable-next-line no-await-in-loop - webhook = await this.mapper.get(Stripe.webhookRedisKey(id)); - - if (forceRecreate) { - // eslint-disable-next-line no-await-in-loop - await this.redis.del(Stripe.webhookRedisKey(id)); - - if (webhook !== null) { - // eslint-disable-next-line no-await-in-loop - await Promise - .resolve(this.request('webhookEndpoints.del', [webhook.stripeId])) - .catch({ statusCode: 404 }, noop); - } - - webhook = null; - } - - if (webhook === null) { - // eslint-disable-next-line no-await-in-loop - await this.upsertWebhook(webhookConfig); - - // eslint-disable-next-line no-continue - continue; - } - - // note can't update api version - if (webhook.url !== url || !isEqual(sortBy(webhook.enabledEvents), sortBy(enabledEvents))) { - // eslint-disable-next-line no-await-in-loop - await this.upsertWebhook(webhookConfig, webhook.stripeId); - } - } - } - - async upsertWebhook(config, stripeId) { - const { id, url, enabledEvents, apiVersion } = config; - const stripeWebhookParams = { - url, - enabled_events: enabledEvents, - api_version: apiVersion, - }; - const stripeWebhook = stripeId === undefined - ? await this.request('webhookEndpoints.create', [stripeWebhookParams], `webhook:create:${id}:${apiVersion}`) - : await this.request('webhookEndpoints.update', [stripeId, stripeWebhookParams]); - const webhook = { - createAt: moment().format(), - secret: stripeWebhook.secret, - stripeId: stripeWebhook.id, - stripeMetada: JSON.stringify(stripeWebhook), - ...config, - }; - - await this.redis.hmset(Stripe.webhookRedisKey(id), webhook); - - return webhook; - } - - async getEventFromRequest(internalWebhookId, request) { - assertStringNotEmpty(internalWebhookId, 'internalWebhookId is invalid'); - assertStringNotEmpty(request.headers['stripe-signature'], 'stripe signature is invalid'); - - const { secret } = await this.mapper.get(Stripe.webhookRedisKey(internalWebhookId), ['secret']); - const sig = request.headers['stripe-signature']; - - return this.client.webhooks.constructEvent(request.params, sig, secret); - } -} - -Stripe.CUSTOMER_FIELDS_FOR_SAVE = ['id', 'default_source', 'email']; - -module.exports = Stripe; diff --git a/test/.env.example b/test/.env.example index 4bd09855..e69de29b 100644 --- a/test/.env.example +++ b/test/.env.example @@ -1,2 +0,0 @@ -MS_PAYMENTS__STRIPE__publicKey=[REPLACE_IT_WITH_YOUR_PUBLIC_KEY__pk_test_...] -MS_PAYMENTS__STRIPE__secretKey=[REPLACE_IT_WITH_YOUR_PSECRET_KEY__sk_test_...] diff --git a/test/e2e/suites/04-balance-util.js b/test/e2e/suites/04-balance-util.js deleted file mode 100644 index 8e6e02af..00000000 --- a/test/e2e/suites/04-balance-util.js +++ /dev/null @@ -1,179 +0,0 @@ -const assert = require('assert'); - -const randomOwner = require('../../helpers/random-owner'); - -describe('balance utils', function suite() { - const Payments = require('../../../src'); - const Balance = require('../../../src/utils/balance'); - const service = new Payments(); - - before('start service', () => service.connect()); - - it('should throw error if params for redis keys are invalid', () => { - assert.throws(() => Balance.userBalanceKey(12345), { message: 'owner is invalid' }); - assert.throws(() => Balance.userBalanceKey(''), { message: 'owner is invalid' }); - assert.throws(() => Balance.userBalanceIncrementIdempotencyKey(12345), { message: 'owner is invalid' }); - assert.throws(() => Balance.userBalanceIncrementIdempotencyKey(''), { message: 'owner is invalid' }); - assert.throws(() => Balance.userBalanceDecrementIdempotencyKey(12345), { message: 'owner is invalid' }); - assert.throws(() => Balance.userBalanceDecrementIdempotencyKey(''), { message: 'owner is invalid' }); - assert.throws(() => Balance.userBalanceGoalKey(12345), { message: 'owner is invalid' }); - assert.throws(() => Balance.userBalanceGoalKey(''), { message: 'owner is invalid' }); - }); - - it('should return keys for redis', () => { - assert.strictEqual(Balance.userBalanceKey('12345'), '12345:balance'); - assert.strictEqual(Balance.userBalanceIncrementIdempotencyKey('12345'), '12345:balance:increment:idempotency'); - assert.strictEqual(Balance.userBalanceDecrementIdempotencyKey('12345'), '12345:balance:decrement:idempotency'); - assert.strictEqual(Balance.userBalanceGoalKey('12345'), '12345:balance:goal'); - }); - - it('should throw error if params for getBalance are invalid', async () => { - const balance = new Balance(service.redis); - - await assert.rejects(balance.get(12345), { message: 'owner is invalid' }); - await assert.rejects(balance.get(''), { message: 'owner is invalid' }); - }); - - it('should return 0 if account balance is not set', async () => { - const balance = new Balance(service.redis); - const owner = randomOwner(); - - assert.strictEqual(await balance.get(owner), 0); - }); - - it('should return account balance', async () => { - const balance = new Balance(service.redis); - const owner = randomOwner(); - - await service.redis.set(Balance.userBalanceKey(owner), 123); - - assert.strictEqual(await balance.get(owner), 123); - }); - - it('should throw error if account balance was corrupted', async () => { - const balance = new Balance(service.redis); - const owner = randomOwner(); - - await service.redis.set(Balance.userBalanceKey(owner), 'perchik is a fat cat'); - - await assert.rejects(balance.get(owner), { message: 'balance is invalid' }); - }); - - it('should throw error if params for increment are invalid', async () => { - const balance = new Balance(service.redis); - const owner = randomOwner(); - - await assert.rejects(balance.increment(12345, 100, 't:12345', 'g:12345'), { message: 'owner is invalid' }); - await assert.rejects(balance.increment(owner, 100.01, 't:12345', 'g:12345'), { message: 'amount is invalid' }); - await assert.rejects(balance.increment(owner, '100', 't:12345', 'g:12345'), { message: 'amount is invalid' }); - await assert.rejects(balance.increment(owner, 100), { message: 'idempotency is invalid' }); - await assert.rejects(balance.increment(owner, 100, ''), { message: 'idempotency is invalid' }); - await assert.rejects(balance.increment(owner, 100, 't:12345'), { message: 'goal is invalid' }); - await assert.rejects(balance.increment(owner, 100, 't:12345', ''), { message: 'goal is invalid' }); - await assert.rejects(balance.increment(owner, 100, 't:12345', 'g:12345', 'pipeline'), { message: 'redis pipeline is invalid' }); - }); - - it('should increment account balance', async () => { - const balance = new Balance(service.redis); - const owner = randomOwner(); - - // 1 - assert.strictEqual(await balance.increment(owner, 10001, 't:10001', 'g:10001'), 10001); - - assert.strictEqual(await balance.get(owner), 10001); - - assert.strictEqual(await service.redis.get(`${owner}:balance`), '10001'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:increment:idempotency`, 't:10001'), '10001'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:goal`, 'g:10001'), '10001'); - - // 2 - await assert.rejects(balance.increment(owner, 10002, 't:10001', 'g:10001'), { message: '409' }); - - assert.strictEqual(await balance.get(owner), 10001); - - assert.strictEqual(await service.redis.get(`${owner}:balance`), '10001'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:increment:idempotency`, 't:10001'), '10001'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:goal`, 'g:10001'), '10001'); - - // 3 - assert.strictEqual(await balance.increment(owner, 10002, 't:10002', 'g:10001'), 20003); - - assert.strictEqual(await balance.get(owner), 20003); - - assert.strictEqual(await service.redis.get(`${owner}:balance`), '20003'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:increment:idempotency`, 't:10001'), '10001'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:increment:idempotency`, 't:10002'), '10002'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:goal`, 'g:10001'), '20003'); - }); - - it('should increment account balance using pipeline', async () => { - const balance = new Balance(service.redis); - const pipeline = service.redis.pipeline(); - const owner = randomOwner(); - - await balance.increment(owner, 10001, 't:10001', 'g:10001', pipeline); - await pipeline.exec(); - - assert.strictEqual(await balance.get(owner), 10001); - }); - - it('should decrement account balance', async () => { - const balance = new Balance(service.redis); - const owner = randomOwner(); - - assert.strictEqual(await balance.increment(owner, 200, 't:10001', 'g:10001'), 200); - - // 1 wrong goal - await assert.rejects(balance.decrement(owner, 200, 't:10001', 'g:10002'), { message: '409' }); - - assert.strictEqual(await balance.get(owner), 200); - - assert.strictEqual(await service.redis.get(`${owner}:balance`), '200'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:increment:idempotency`, 't:10001'), '200'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:decrement:idempotency`, 't:10001'), null); - assert.strictEqual(await service.redis.hget(`${owner}:balance:goal`, 'g:10001'), '200'); - - // 2 wrong goal amount - await assert.rejects(balance.decrement(owner, 100, 't:10001', 'g:10001'), { message: '409' }); - - assert.strictEqual(await balance.get(owner), 200); - - assert.strictEqual(await service.redis.get(`${owner}:balance`), '200'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:increment:idempotency`, 't:10001'), '200'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:decrement:idempotency`, 't:10001'), null); - assert.strictEqual(await service.redis.hget(`${owner}:balance:goal`, 'g:10001'), '200'); - - // 3 decrement - assert.strictEqual(await balance.decrement(owner, 200, 't:10001', 'g:10001'), 0); - - assert.strictEqual(await balance.get(owner), 0); - - assert.strictEqual(await service.redis.get(`${owner}:balance`), '0'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:increment:idempotency`, 't:10001'), '200'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:decrement:idempotency`, 't:10001'), '200'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:goal`, 'g:10001'), null); - - // 4 wrong idempotency - await assert.rejects(balance.decrement(owner, 200, 't:10001', 'g:10001'), { message: '409' }); - - assert.strictEqual(await balance.get(owner), 0); - - assert.strictEqual(await service.redis.get(`${owner}:balance`), '0'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:increment:idempotency`, 't:10001'), '200'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:decrement:idempotency`, 't:10001'), '200'); - assert.strictEqual(await service.redis.hget(`${owner}:balance:goal`, 'g:10001'), null); - }); - - it('should decrement account balance using pipeline', async () => { - const balance = new Balance(service.redis); - const pipeline = service.redis.pipeline(); - const owner = randomOwner(); - - await balance.increment(owner, 10001, 't:10001', 'g:10001'); - - await balance.decrement(owner, 10001, 't:10001', 'g:10001', pipeline); - await pipeline.exec(); - - assert.strictEqual(await balance.get(owner), 0); - }); -}); diff --git a/test/e2e/suites/08-migrations.js b/test/e2e/suites/04-migrations.js similarity index 100% rename from test/e2e/suites/08-migrations.js rename to test/e2e/suites/04-migrations.js diff --git a/test/e2e/suites/05-balance-action.js b/test/e2e/suites/05-balance-action.js deleted file mode 100644 index 78978a2c..00000000 --- a/test/e2e/suites/05-balance-action.js +++ /dev/null @@ -1,105 +0,0 @@ -const assert = require('assert'); -const request = require('request-promise'); - -const randomOwner = require('../../helpers/random-owner'); -const { getToken, makeHeader } = require('../../helpers/auth'); - -describe('balance actions', function suite() { - const Payments = require('../../../src'); - const Balance = require('../../../src/utils/balance'); - const service = new Payments(); - - before('start service', () => service.connect()); - - before(async () => { - this.admin0 = (await getToken.call(service, 'test@test.ru')); - this.user0 = (await getToken.call(service, 'user0@test.com')); - }); - - afterEach(() => service.redis.del(Balance.userBalanceKey(this.user0.user.id))); - - it('should return error if auth header is not present', async () => { - const { body } = await request.get( - 'http://localhost:3000/payments/balance/get', - { qs: { owner: 'user0' }, resolveWithFullResponse: true, simple: false } - ); - - assert.strictEqual(body, '{"statusCode":401,"error":"Unauthorized","message":"An attempt was made to perform' - + ' an operation without authentication: Credentials Required","name":"AuthenticationRequiredError"}'); - }); - - it('should return error if owner is empty string', async () => { - const { body } = await request.get( - 'http://localhost:3000/payments/balance/get', - { qs: { owner: '' }, headers: makeHeader(this.user0.jwt), resolveWithFullResponse: true, simple: false } - ); - - assert.strictEqual(body, '{"statusCode":400,"error":"Bad Request","message":"balance.get validation failed:' - + ' data.owner should NOT be shorter than 1 characters","name":"HttpStatusError"}'); - }); - - it('should return error if not admin', async () => { - const { body } = await request.get( - 'http://localhost:3000/payments/balance/get', - { qs: { owner: 'admin0' }, headers: makeHeader(this.user0.jwt), resolveWithFullResponse: true, simple: false } - ); - - assert.strictEqual(body, '{"statusCode":403,"error":"Forbidden","message":"not enough rights","name":"HttpStatusError"}'); - }); - - it('should return error if owner does not exist', async () => { - const { body } = await request.get( - 'http://localhost:3000/payments/balance/get', - { qs: { owner: 'fat-cat' }, headers: makeHeader(this.admin0.jwt), resolveWithFullResponse: true, simple: false } - ); - - assert.strictEqual(body, '{"statusCode":404,"error":"Not Found","message":"user not found","name":"HttpStatusError"}'); - }); - - it('should return 0 if account balance was not set', async () => { - const { body } = await request.get( - 'http://localhost:3000/payments/balance/get', - { headers: makeHeader(this.user0.jwt), resolveWithFullResponse: true, simple: false } - ); - - assert.strictEqual(body, '{"data":{"type":"balance","id":"user0","attributes":{"value":0}}}'); - }); - - it('should return user balance requested by owner', async () => { - await service.redis.set(Balance.userBalanceKey(this.user0.user.id), 1449); - - const { body } = await request.get( - 'http://localhost:3000/payments/balance/get', - { headers: makeHeader(this.user0.jwt), resolveWithFullResponse: true, simple: false } - ); - - assert.strictEqual(body, '{"data":{"type":"balance","id":"user0","attributes":{"value":1449}}}'); - }); - - it('should return user balance requested by admin', async () => { - await service.redis.set(Balance.userBalanceKey(this.user0.user.id), 1448); - - const { body } = await request.get( - 'http://localhost:3000/payments/balance/get', - { qs: { owner: 'user0' }, headers: makeHeader(this.admin0.jwt), resolveWithFullResponse: true, simple: false } - ); - - assert.strictEqual(body, '{"data":{"type":"balance","id":"user0","attributes":{"value":1448}}}'); - }); - - it('should decrement balance', async () => { - const owner = randomOwner(); - - await service.balance.increment(owner, 99, 'icr#1', 'goal1'); - - const response = await service.amqp.publishAndWait('payments.balance.decrement', { - ownerId: owner, - amount: 99, - idempotency: 'decr#1', - goal: 'goal1', - }); - - assert.strictEqual(response, 0); - assert.strictEqual(await service.balance.get(owner), 0); - }); -}); diff --git a/test/e2e/suites/06-stripe.js b/test/e2e/suites/06-stripe.js deleted file mode 100644 index 8b3099b6..00000000 --- a/test/e2e/suites/06-stripe.js +++ /dev/null @@ -1,445 +0,0 @@ -const { strictEqual, deepStrictEqual } = require('assert'); -const request = require('request-promise'); -const fs = require('fs'); -const path = require('path'); -const replace = require('lodash/replace'); -const { inspectPromise } = require('@makeomatic/deploy'); - -const { createSignature } = require('../../helpers/stripe'); -const { getToken, makeHeader } = require('../../helpers/auth'); -const { isUUIDv4 } = require('../../helpers/uuid'); - -describe('stripe', function suite() { - const Payments = require('../../../src'); - const Charge = require('../../../src/utils/charge'); - const service = new Payments({ stripe: { enabled: true, webhook: { enabled: true } } }); - let successChargeId; - let failChargeId; - - before('start service', () => service.connect()); - before(async () => { - this.admin0 = (await getToken.call(service, 'test@test.ru')); - this.user0 = (await getToken.call(service, 'user0@test.com')); - }); - - after(async () => { - await service.redis.del(`${this.user0.user.id}:balance`); - await service.redis.del(`${this.user0.user.id}:charges`); - await service.redis.del(`charge:${successChargeId}`); - await service.redis.del(`charge:${failChargeId}`); - }); - - describe('create action', () => { - it('should create success stripe charge', async () => { - const response = await request.post({ - url: 'http://localhost:3000/payments/charge/stripe/create', - body: { - token: 'tok_mastercard', - amount: 1001, - description: 'Feed the cat', - saveCard: true, - email: 'perchik@cat.com' }, - headers: makeHeader(this.user0.jwt), - json: true }); - - strictEqual(isUUIDv4(response.data.id), true); - strictEqual(response.data.type, 'charge'); - strictEqual(response.data.attributes.amount, 1001); - strictEqual(response.data.attributes.description, 'Feed the cat'); - strictEqual(response.data.attributes.status, 0); - strictEqual(response.data.attributes.createAt !== undefined, true); - strictEqual(response.data.attributes.owner, 'user0'); - strictEqual(response.data.attributes.failReason, ''); - - successChargeId = response.data.id; - - const charge = await service.redis.hgetall(`charge:${successChargeId}`); - - strictEqual(charge.id, successChargeId); - strictEqual(charge.amount, '1001'); - strictEqual(charge.description, 'Feed the cat'); - strictEqual(charge.owner, this.user0.user.id); - strictEqual(charge.createAt !== undefined, true); - strictEqual(charge.status, '0'); - strictEqual(charge.failReason, ''); - strictEqual(charge.metadata, `{"owner":"${this.user0.user.id}","amount":1001,"description":"Feed the cat",` - + '"saveCard":true,"email":"perchik@cat.com","metadata":{}}'); - strictEqual(charge.source, 'stripe'); - strictEqual(charge.sourceId, ''); - strictEqual(charge.sourceMetadata, ''); - }); - - it('should complete charge and increase balance', async () => { - const secret = await service.redis.hget('stripe:webhook:charge', 'secret'); - const payload = replace( - fs.readFileSync(path.resolve(__dirname, '../../helpers/mocks/stripe-webhook-successed.json'), 'utf8'), - /{{INTERNAL_ID_PLACEHOLDER}}/g, - successChargeId - ); - const signature = createSignature(payload, secret); - const response = await request.post({ - url: 'http://localhost:3000/payments/charge/stripe/webhook', - body: payload, - headers: { 'stripe-signature': signature } }); - - deepStrictEqual(response, '{"received":true}'); - - const charge = await service.redis.hgetall(`charge:${successChargeId}`); - - strictEqual(charge.id, successChargeId); - strictEqual(charge.amount, '1001'); - strictEqual(charge.description, 'Feed the cat'); - strictEqual(charge.owner, this.user0.user.id); - strictEqual(charge.createAt !== undefined, true); - strictEqual(charge.status, String(Charge.STATUS_COMPLETED)); - strictEqual(charge.failReason, ''); - strictEqual(charge.metadata, `{"owner":"${this.user0.user.id}","amount":1001,"description":"Feed the cat",` - + '"saveCard":true,"email":"perchik@cat.com","metadata":{}}'); - strictEqual(charge.source, 'stripe'); - strictEqual(charge.sourceId, 'ch_1EKvTOAbVbjCtGTWjanopixO'); - strictEqual(charge.sourceMetadata !== '', true); - - const balance = await service.redis.get(`${this.user0.user.id}:balance`); - - strictEqual(balance, '1001'); - }); - - it('should create failed stripe charge', async () => { - const response = await request.post({ - url: 'http://localhost:3000/payments/charge/stripe/create', - body: { - token: 'tok_cvcCheckFail', - amount: 100002, - description: 'Feed the cat!!!!' }, - headers: makeHeader(this.user0.jwt), - json: true }); - - strictEqual(isUUIDv4(response.data.id), true); - strictEqual(response.data.type, 'charge'); - strictEqual(response.data.attributes.amount, 100002); - strictEqual(response.data.attributes.description, 'Feed the cat!!!!'); - strictEqual(response.data.attributes.status, 0); - strictEqual(response.data.attributes.createAt !== undefined, true); - strictEqual(response.data.attributes.owner, 'user0'); - strictEqual(response.data.attributes.failReason, ''); - - failChargeId = response.data.id; - - const charge = await service.redis.hgetall(`charge:${failChargeId}`); - - strictEqual(charge.id, failChargeId); - strictEqual(charge.amount, '100002'); - strictEqual(charge.description, 'Feed the cat!!!!'); - strictEqual(charge.owner, this.user0.user.id); - strictEqual(charge.createAt !== undefined, true); - strictEqual(charge.status, '0'); - strictEqual(charge.failReason, ''); - strictEqual(charge.metadata, `{"owner":"${this.user0.user.id}","amount":100002,"description":"Feed the cat!!!!",` - + '"saveCard":false,"metadata":{}}'); - strictEqual(charge.source, 'stripe'); - strictEqual(charge.sourceId, ''); - strictEqual(charge.sourceMetadata, ''); - }); - - it('should fail charge', async () => { - const secret = await service.redis.hget('stripe:webhook:charge', 'secret'); - const payload = replace( - fs.readFileSync(path.resolve(__dirname, '../../helpers/mocks/stripe-webhook-failed.json'), 'utf8'), - /{{INTERNAL_ID_PLACEHOLDER}}/g, - failChargeId - ); - const signature = createSignature(payload, secret); - const response = await request.post({ - url: 'http://localhost:3000/payments/charge/stripe/webhook', - body: payload, - headers: { 'stripe-signature': signature } }); - - deepStrictEqual(response, '{"received":true}'); - - const charge = await service.redis.hgetall(`charge:${failChargeId}`); - - strictEqual(charge.id, failChargeId); - strictEqual(charge.amount, '100002'); - strictEqual(charge.description, 'Feed the cat!!!!'); - strictEqual(charge.owner, this.user0.user.id); - strictEqual(charge.createAt !== undefined, true); - strictEqual(charge.status, '1'); - strictEqual(charge.failReason, 'Your card\'s security code is incorrect.'); - strictEqual(charge.metadata, `{"owner":"${this.user0.user.id}","amount":100002,"description":"Feed the cat!!!!",` - + '"saveCard":false,"metadata":{}}'); - strictEqual(charge.source, 'stripe'); - strictEqual(charge.sourceId, 'ch_1ELBVGAbVbjCtGTWMdFR7AYA'); - strictEqual(charge.sourceMetadata !== '', true); - - const balance = await service.redis.get(`${this.user0.user.id}:balance`); - - // NOTE: did not change - strictEqual(balance, '1001'); - }); - }); - - describe('list action', () => { - describe('http', () => { - it('should return error if not admin', async () => { - const response = await request.get({ - url: 'http://localhost:3000/payments/charge/list', - qs: { owner: 'admin0' }, - headers: makeHeader(this.user0.jwt), - simple: false }); - - strictEqual(response, '{"statusCode":403,"error":"Forbidden","message":"not enough rights","name":"HttpStatusError"}'); - }); - - it('should be able to get charges list by user', async () => { - const response = await request.get({ - url: 'http://localhost:3000/payments/charge/list', - qs: { owner: 'user0' }, - headers: makeHeader(this.user0.jwt), - json: true }); - - strictEqual(response.meta.offset, 0); - strictEqual(response.meta.limit, 20); - strictEqual(response.meta.cursor, 20); - strictEqual(response.meta.page, 1); - strictEqual(response.meta.pages, 1); - - strictEqual(response.data.length, 2); - - strictEqual(response.data[0].id, failChargeId); - strictEqual(response.data[0].type, 'charge'); - strictEqual(response.data[0].attributes.amount, '100002'); - strictEqual(response.data[0].attributes.description, 'Feed the cat!!!!'); - strictEqual(response.data[0].attributes.owner, 'user0'); - strictEqual(response.data[0].attributes.createAt !== undefined, true); - strictEqual(response.data[0].attributes.status, '1'); - strictEqual(response.data[0].attributes.failReason, 'Your card\'s security code is incorrect.'); - strictEqual(response.data[0].attributes.metadata === undefined, true); - strictEqual(response.data[0].attributes.source === undefined, true); - strictEqual(response.data[0].attributes.sourceId === undefined, true); - strictEqual(response.data[0].attributes.sourceMetadata === undefined, true); - strictEqual(response.data[0].attributes.failMetadata === undefined, true); - - strictEqual(response.data[1].id, successChargeId); - strictEqual(response.data[1].type, 'charge'); - strictEqual(response.data[1].attributes.amount, '1001'); - strictEqual(response.data[1].attributes.description, 'Feed the cat'); - strictEqual(response.data[1].attributes.owner, 'user0'); - strictEqual(response.data[1].attributes.createAt !== undefined, true); - strictEqual(response.data[1].attributes.status, String(Charge.STATUS_COMPLETED)); - strictEqual(response.data[1].attributes.failReason, ''); - strictEqual(response.data[1].attributes.metadata === undefined, true); - strictEqual(response.data[1].attributes.source === undefined, true); - strictEqual(response.data[1].attributes.sourceId === undefined, true); - strictEqual(response.data[1].attributes.sourceMetadata === undefined, true); - strictEqual(response.data[1].attributes.failMetadata === undefined, true); - }); - - it('should be able to get charges list by admin', async () => { - const response = await request.get({ - url: 'http://localhost:3000/payments/charge/list', - qs: { owner: 'user0' }, - headers: makeHeader(this.admin0.jwt), - json: true }); - - strictEqual(response.meta.offset, 0); - strictEqual(response.meta.limit, 20); - strictEqual(response.meta.cursor, 20); - strictEqual(response.meta.page, 1); - strictEqual(response.meta.pages, 1); - - strictEqual(response.data.length, 2); - - strictEqual(response.data[0].id, failChargeId); - strictEqual(response.data[0].type, 'charge'); - strictEqual(response.data[0].attributes.amount, '100002'); - strictEqual(response.data[0].attributes.description, 'Feed the cat!!!!'); - strictEqual(response.data[0].attributes.owner, 'user0'); - strictEqual(response.data[0].attributes.createAt !== undefined, true); - strictEqual(response.data[0].attributes.status, '1'); - strictEqual(response.data[0].attributes.failReason, 'Your card\'s security code is incorrect.'); - strictEqual(response.data[0].attributes.metadata === undefined, true); - strictEqual(response.data[0].attributes.source === undefined, true); - strictEqual(response.data[0].attributes.sourceId === undefined, true); - strictEqual(response.data[0].attributes.sourceMetadata === undefined, true); - strictEqual(response.data[0].attributes.failMetadata === undefined, true); - - strictEqual(response.data[1].id, successChargeId); - strictEqual(response.data[1].type, 'charge'); - strictEqual(response.data[1].attributes.amount, '1001'); - strictEqual(response.data[1].attributes.description, 'Feed the cat'); - strictEqual(response.data[1].attributes.owner, 'user0'); - strictEqual(response.data[1].attributes.createAt !== undefined, true); - strictEqual(response.data[1].attributes.status, String(Charge.STATUS_COMPLETED)); - strictEqual(response.data[1].attributes.failReason, ''); - strictEqual(response.data[1].attributes.metadata === undefined, true); - strictEqual(response.data[1].attributes.source === undefined, true); - strictEqual(response.data[1].attributes.sourceId === undefined, true); - strictEqual(response.data[1].attributes.sourceMetadata === undefined, true); - strictEqual(response.data[1].attributes.failMetadata === undefined, true); - }); - }); - - describe('amqp', () => { - it('should return error if not admin', async () => { - const error = await service.amqp - .publishAndWait( - 'payments.charge.list', - { owner: 'admin0' }, - { headers: makeHeader(this.user0.jwt) } - ) - .reflect() - .then(inspectPromise(false)); - - strictEqual(error.statusCode, 403); - strictEqual(error.message, 'not enough rights'); - strictEqual(error.name, 'HttpStatusError'); - }); - - it('should be able to get charges list by user', async () => { - const response = await service.amqp.publishAndWait( - 'payments.charge.list', - { owner: 'user0' }, - { headers: makeHeader(this.user0.jwt) } - ); - - strictEqual(response.meta.offset, 0); - strictEqual(response.meta.limit, 20); - strictEqual(response.meta.cursor, 20); - strictEqual(response.meta.page, 1); - strictEqual(response.meta.pages, 1); - - strictEqual(response.data.length, 2); - - strictEqual(response.data[0].id, failChargeId); - strictEqual(response.data[0].type, 'charge'); - strictEqual(response.data[0].attributes.amount, '100002'); - strictEqual(response.data[0].attributes.description, 'Feed the cat!!!!'); - strictEqual(response.data[0].attributes.owner, 'user0'); - strictEqual(response.data[0].attributes.createAt !== undefined, true); - strictEqual(response.data[0].attributes.status, '1'); - strictEqual(response.data[0].attributes.failReason, 'Your card\'s security code is incorrect.'); - strictEqual(response.data[0].attributes.metadata === undefined, true); - strictEqual(response.data[0].attributes.source === undefined, true); - strictEqual(response.data[0].attributes.sourceId === undefined, true); - strictEqual(response.data[0].attributes.sourceMetadata === undefined, true); - strictEqual(response.data[0].attributes.failMetadata === undefined, true); - - strictEqual(response.data[1].id, successChargeId); - strictEqual(response.data[1].type, 'charge'); - strictEqual(response.data[1].attributes.amount, '1001'); - strictEqual(response.data[1].attributes.description, 'Feed the cat'); - strictEqual(response.data[1].attributes.owner, 'user0'); - strictEqual(response.data[1].attributes.createAt !== undefined, true); - strictEqual(response.data[1].attributes.status, String(Charge.STATUS_COMPLETED)); - strictEqual(response.data[1].attributes.failReason, ''); - strictEqual(response.data[1].attributes.metadata === undefined, true); - strictEqual(response.data[1].attributes.source === undefined, true); - strictEqual(response.data[1].attributes.sourceId === undefined, true); - strictEqual(response.data[1].attributes.sourceMetadata === undefined, true); - strictEqual(response.data[1].attributes.failMetadata === undefined, true); - }); - - it('should be able to get charges list by admin', async () => { - const response = await service.amqp.publishAndWait( - 'payments.charge.list', - { owner: 'user0' }, - { headers: makeHeader(this.admin0.jwt) } - ); - - strictEqual(response.meta.offset, 0); - strictEqual(response.meta.limit, 20); - strictEqual(response.meta.cursor, 20); - strictEqual(response.meta.page, 1); - strictEqual(response.meta.pages, 1); - - strictEqual(response.data.length, 2); - - strictEqual(response.data[0].id, failChargeId); - strictEqual(response.data[0].type, 'charge'); - strictEqual(response.data[0].attributes.amount, '100002'); - strictEqual(response.data[0].attributes.description, 'Feed the cat!!!!'); - strictEqual(response.data[0].attributes.owner, 'user0'); - strictEqual(response.data[0].attributes.createAt !== undefined, true); - strictEqual(response.data[0].attributes.status, '1'); - strictEqual(response.data[0].attributes.failReason, 'Your card\'s security code is incorrect.'); - strictEqual(response.data[0].attributes.metadata === undefined, true); - strictEqual(response.data[0].attributes.source === undefined, true); - strictEqual(response.data[0].attributes.sourceId === undefined, true); - strictEqual(response.data[0].attributes.sourceMetadata === undefined, true); - strictEqual(response.data[0].attributes.failMetadata === undefined, true); - - strictEqual(response.data[1].id, successChargeId); - strictEqual(response.data[1].type, 'charge'); - strictEqual(response.data[1].attributes.amount, '1001'); - strictEqual(response.data[1].attributes.description, 'Feed the cat'); - strictEqual(response.data[1].attributes.owner, 'user0'); - strictEqual(response.data[1].attributes.createAt !== undefined, true); - strictEqual(response.data[1].attributes.status, String(Charge.STATUS_COMPLETED)); - strictEqual(response.data[1].attributes.failReason, ''); - strictEqual(response.data[1].attributes.metadata === undefined, true); - strictEqual(response.data[1].attributes.source === undefined, true); - strictEqual(response.data[1].attributes.sourceId === undefined, true); - strictEqual(response.data[1].attributes.sourceMetadata === undefined, true); - strictEqual(response.data[1].attributes.failMetadata === undefined, true); - }); - }); - }); - - describe('get action', () => { - it('should return error if not admin', async () => { - const charge = await service.charge.create('stripe', this.admin0.user.id, 100, 'test'); - - const response = await request.get({ - url: 'http://localhost:3000/payments/charge/get', - qs: { id: charge.id }, - headers: makeHeader(this.user0.jwt), - simple: false }); - - strictEqual(response, '{"statusCode":403,"error":"Forbidden","message":"not enough rights","name":"HttpStatusError"}'); - }); - - it('should be able to get charge by user', async () => { - const response = await request.get({ - url: 'http://localhost:3000/payments/charge/get', - qs: { id: successChargeId }, - headers: makeHeader(this.user0.jwt), - json: true }); - - strictEqual(response.data.id, successChargeId); - strictEqual(response.data.type, 'charge'); - strictEqual(response.data.attributes.amount, '1001'); - strictEqual(response.data.attributes.description, 'Feed the cat'); - strictEqual(response.data.attributes.owner, '[[protected]]'); - strictEqual(response.data.attributes.createAt !== undefined, true); - strictEqual(response.data.attributes.status, Charge.STATUS_COMPLETED); - strictEqual(response.data.attributes.failReason, ''); - strictEqual(response.data.attributes.metadata === undefined, true); - strictEqual(response.data.attributes.source === undefined, true); - strictEqual(response.data.attributes.sourceId === undefined, true); - strictEqual(response.data.attributes.sourceMetadata === undefined, true); - strictEqual(response.data.attributes.failMetadata === undefined, true); - }); - - it('should be able to get charge by admin', async () => { - const response = await request.get({ - url: 'http://localhost:3000/payments/charge/get', - qs: { id: failChargeId }, - headers: makeHeader(this.admin0.jwt), - json: true }); - - strictEqual(response.data.id, failChargeId); - strictEqual(response.data.type, 'charge'); - strictEqual(response.data.attributes.amount, '100002'); - strictEqual(response.data.attributes.description, 'Feed the cat!!!!'); - strictEqual(response.data.attributes.owner, '[[protected]]'); - strictEqual(response.data.attributes.createAt !== undefined, true); - strictEqual(response.data.attributes.status, 1); - strictEqual(response.data.attributes.failReason, 'Your card\'s security code is incorrect.'); - strictEqual(response.data.attributes.metadata === undefined, true); - strictEqual(response.data.attributes.source === undefined, true); - strictEqual(response.data.attributes.sourceId === undefined, true); - strictEqual(response.data.attributes.sourceMetadata === undefined, true); - strictEqual(response.data.attributes.failMetadata === undefined, true); - }); - }); -}); diff --git a/test/e2e/suites/07-charge-paypal.js b/test/e2e/suites/07-charge-paypal.js deleted file mode 100644 index 7876f1ad..00000000 --- a/test/e2e/suites/07-charge-paypal.js +++ /dev/null @@ -1,211 +0,0 @@ -const request = require('request-promise'); -const assert = require('assert'); - -const { strictEqual } = assert; -const { getToken, makeHeader } = require('../../helpers/auth'); -const { isUUIDv4 } = require('../../helpers/uuid'); -const { initChrome, closeChrome, approveSale } = require('../../helpers/chrome'); - -describe('charge.paypal', function suite() { - const Payments = require('../../../src'); - const Charge = require('../../../src/utils/charge'); - const service = new Payments(); - - before('start service', () => service.connect()); - before('get user tokens', async () => { - this.admin0 = await getToken.call(service, 'test@test.ru'); - this.user0 = await getToken.call(service, 'user0@test.com'); - }); - - after(async () => { - await service.redis.del(`${this.user0.user.id}:balance`); - await service.redis.del(`${this.user0.user.id}:charges`); - }); - - describe('create action', () => { - let approvalUrl; - let PayerID; - let paymentId; - let PaymentToken; - - beforeEach('init Chrome', initChrome); - afterEach('close chrome', closeChrome); - - describe('http', () => { - it('should create success paypal charge', async () => { - const response = await request.post({ - url: 'http://localhost:3000/payments/charge/paypal/create', - body: { - amount: 1005, - description: 'Feed the cat', - returnUrl: 'http://api-sandbox.cappasity.matic.ninja/paypal-payments-return', - cancelUrl: 'http://api-sandbox.cappasity.matic.ninja/paypal-payments-cancel' }, - headers: makeHeader(this.user0.jwt), - json: true }); - - strictEqual(isUUIDv4(response.data.id), true); - strictEqual(response.data.type, 'charge'); - strictEqual(response.data.attributes.amount, 1005); - strictEqual(response.data.attributes.description, 'Feed the cat'); - strictEqual(response.data.attributes.status, 0); - strictEqual(response.data.attributes.createAt !== undefined, true); - strictEqual(response.data.attributes.owner, 'user0'); - strictEqual(response.data.attributes.failReason, ''); - strictEqual( - response.meta.paypal.approvalUrl.href.includes('https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token='), - true - ); - strictEqual(response.meta.paypal.approvalUrl.rel, 'approval_url'); - strictEqual(response.meta.paypal.approvalUrl.method, 'REDIRECT'); - strictEqual(response.meta.paypal.paymentId.includes('PAYID'), true); - - const successChargeId = response.data.id; - const charge = await service.redis.hgetall(`charge:${successChargeId}`); - const internalId = await service.redis.hget('paypal-payment:internal:ids', charge.sourceId); - - strictEqual(internalId, successChargeId); - strictEqual(charge.id, successChargeId); - strictEqual(charge.amount, '1005'); - strictEqual(charge.description, 'Feed the cat'); - strictEqual(charge.owner, this.user0.user.id); - strictEqual(charge.createAt !== undefined, true); - strictEqual(charge.status, '0'); - strictEqual(charge.failReason, ''); - strictEqual(charge.metadata, `{"owner":"${this.user0.user.id}","amount":1005,` - + '"description":"Feed the cat","returnUrl":"http://api-sandbox.cappasity.matic.ninja/paypal-payments-return",' - + '"cancelUrl":"http://api-sandbox.cappasity.matic.ninja/paypal-payments-cancel"}'); - strictEqual(charge.source, 'paypal'); - strictEqual(charge.sourceId.includes('PAYID'), true); - strictEqual( - charge.sourceMetadata.includes( - '"intent":"authorize","state":"created","payer":{"payment_method":"paypal"},' - + '"transactions":[{"amount":{"total":"10.05","currency":"USD"},' - + `"description":"Feed the cat","custom":"${successChargeId}"` - ), - true - ); - }); - }); - - describe('amqp', () => { - beforeEach('should create success paypal charge', async () => { - const response = await service.amqp.publishAndWait( - 'payments.charge.paypal.create', - { - amount: 1005, - description: 'Feed the cat', - returnUrl: 'http://api-sandbox.cappasity.matic.ninja/paypal-payments-return', - cancelUrl: 'http://api-sandbox.cappasity.matic.ninja/paypal-payments-cancel', - }, - { headers: makeHeader(this.user0.jwt) } - ); - - strictEqual(isUUIDv4(response.data.id), true); - strictEqual(response.data.type, 'charge'); - strictEqual(response.data.attributes.amount, 1005); - strictEqual(response.data.attributes.description, 'Feed the cat'); - strictEqual(response.data.attributes.status, 0); - strictEqual(response.data.attributes.createAt !== undefined, true); - strictEqual(response.data.attributes.owner, 'user0'); - strictEqual(response.data.attributes.failReason, ''); - strictEqual( - response.meta.paypal.approvalUrl.href.includes('https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token='), - true - ); - strictEqual(response.meta.paypal.approvalUrl.rel, 'approval_url'); - strictEqual(response.meta.paypal.approvalUrl.method, 'REDIRECT'); - strictEqual(response.meta.paypal.paymentId.includes('PAYID'), true); - - const successChargeId = response.data.id; - const charge = await service.redis.hgetall(`charge:${successChargeId}`); - const internalId = await service.redis.hget('paypal-payment:internal:ids', charge.sourceId); - - strictEqual(internalId, successChargeId); - strictEqual(charge.id, successChargeId); - strictEqual(charge.amount, '1005'); - strictEqual(charge.description, 'Feed the cat'); - strictEqual(charge.owner, this.user0.user.id); - strictEqual(charge.createAt !== undefined, true); - strictEqual(charge.status, '0'); - strictEqual(charge.failReason, ''); - strictEqual(charge.metadata, `{"owner":"${this.user0.user.id}","amount":1005,` - + '"description":"Feed the cat","returnUrl":"http://api-sandbox.cappasity.matic.ninja/paypal-payments-return",' - + '"cancelUrl":"http://api-sandbox.cappasity.matic.ninja/paypal-payments-cancel"}'); - strictEqual(charge.source, 'paypal'); - strictEqual(charge.sourceId.includes('PAYID'), true); - strictEqual( - charge.sourceMetadata.includes( - '"intent":"authorize","state":"created","payer":{"payment_method":"paypal"},' - + '"transactions":[{"amount":{"total":"10.05","currency":"USD"},' - + `"description":"Feed the cat","custom":"${successChargeId}"` - ), - true - ); - - approvalUrl = response.meta.paypal.approvalUrl.href; - }); - - beforeEach('should approve URL', async () => { - const query = await approveSale(approvalUrl, /paypal-payments-return\?/); - - PayerID = query.payer_id; - paymentId = query.payment_id; - PaymentToken = query.token; - }); - - beforeEach('should execute paypal payment and respond with authorization data', async () => { - const response = await service.amqp.publishAndWait('payments.charge.paypal.return', { - PayerID, - paymentId, - token: PaymentToken, - }, { timeout: 20000 }); - - // normalized format response - assert(response.data); - assert(response.data.id); - assert.equal(response.data.type, 'charge'); - assert(response.data.attributes); - assert.equal(response.data.attributes.status, Charge.STATUS_AUTHORIZED); - - // meta - assert(response.meta); - assert(response.meta.paypal); - assert(response.meta.paypal.payer); - strictEqual(response.meta.paypal.payer.payment_method, 'paypal'); - strictEqual(response.meta.paypal.payer.status, 'VERIFIED'); - assert(response.meta.paypal.payer.payer_info); - strictEqual(response.meta.paypal.payer.payer_info.country_code, 'US'); - }); - - describe('authorize the charge', () => { - it('should capture paypal charge', async () => { - const response = await service.amqp.publishAndWait('payments.charge.paypal.capture', { - paymentId, - }, { timeout: 20000 }); - - assert.ok(response.data); - assert.ok(response.data.id); - assert.equal(response.data.type, 'charge'); - assert.equal(response.data.attributes.status, Charge.STATUS_COMPLETED); - - console.info('%j', response); - }); - }); - - describe('void the charge', () => { - it('should void paypal charge', async () => { - const response = await service.amqp.publishAndWait('payments.charge.paypal.void', { - paymentId, - }, { timeout: 20000 }); - - assert.ok(response.data); - assert.ok(response.data.id); - assert.equal(response.data.type, 'charge'); - assert.equal(response.data.attributes.status, Charge.STATUS_CANCELED); - - console.info('%j', response); - }); - }); - }); - }); -}); diff --git a/test/helpers/mocks/stripe-webhook-failed.json b/test/helpers/mocks/stripe-webhook-failed.json deleted file mode 100644 index 7722ae4b..00000000 --- a/test/helpers/mocks/stripe-webhook-failed.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "id": "evt_1ELBVHAbVbjCtGTWG5sHz9ts", - "object": "event", - "api_version": "2019-03-14", - "created": 1554307194, - "data": { - "object": { - "id": "ch_1ELBVGAbVbjCtGTWMdFR7AYA", - "object": "charge", - "amount": 100002, - "amount_refunded": 0, - "application": null, - "application_fee": null, - "application_fee_amount": null, - "balance_transaction": null, - "billing_details": { - "address": { - "city": null, - "country": null, - "line1": null, - "line2": null, - "postal_code": null, - "state": null - }, - "email": null, - "name": null, - "phone": null - }, - "captured": true, - "created": 1554307194, - "currency": "usd", - "customer": null, - "description": "Feed the cat!!!!", - "destination": null, - "dispute": null, - "failure_code": "incorrect_cvc", - "failure_message": "Your card's security code is incorrect.", - "fraud_details": { - }, - "invoice": null, - "livemode": false, - "metadata": { - "internalId": "{{INTERNAL_ID_PLACEHOLDER}}" - }, - "on_behalf_of": null, - "order": null, - "outcome": { - "network_status": "reversed_after_approval", - "reason": "requested_block_on_incorrect_cvc", - "risk_level": "normal", - "risk_score": 42, - "rule": "block_if_wrong_cvc", - "seller_message": "You requested that Stripe block payments (like this one) for which the customer-entered CVC code does not match the code on file with the card-issuing bank.", - "type": "blocked" - }, - "paid": false, - "payment_intent": null, - "payment_method_details": { - "card": { - "brand": "visa", - "checks": { - "address_line1_check": null, - "address_postal_code_check": null, - "cvc_check": "fail" - }, - "country": "US", - "exp_month": 4, - "exp_year": 2020, - "fingerprint": "q3hQKGq5ZNZpD1ci", - "funding": "credit", - "last4": "0101", - "three_d_secure": null, - "wallet": null - }, - "type": "card" - }, - "receipt_email": null, - "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_1EDc6sAbVbjCtGTW/ch_1ELBVGAbVbjCtGTWMdFR7AYA/rcpt_Eop9SfzLIGpnpiuIU4HYQffzb4G7KKy", - "refunded": false, - "refunds": { - "object": "list", - "data": [ - ], - "has_more": false, - "total_count": 0, - "url": "/v1/charges/ch_1ELBVGAbVbjCtGTWMdFR7AYA/refunds" - }, - "review": null, - "shipping": null, - "source": { - "id": "card_1ELBVGAbVbjCtGTW5M0nvQ2U", - "object": "card", - "address_city": null, - "address_country": null, - "address_line1": null, - "address_line1_check": null, - "address_line2": null, - "address_state": null, - "address_zip": null, - "address_zip_check": null, - "brand": "Visa", - "country": "US", - "customer": null, - "cvc_check": "fail", - "dynamic_last4": null, - "exp_month": 4, - "exp_year": 2020, - "fingerprint": "q3hQKGq5ZNZpD1ci", - "funding": "credit", - "last4": "0101", - "metadata": { - }, - "name": null, - "tokenization_method": null - }, - "source_transfer": null, - "statement_descriptor": null, - "status": "failed", - "transfer_data": null, - "transfer_group": null - } - }, - "livemode": false, - "pending_webhooks": 1, - "request": { - "id": "req_q6hqw8ZWIExtDb", - "idempotency_key": "charge:{{INTERNAL_ID_PLACEHOLDER}}" - }, - "type": "charge.failed" -} diff --git a/test/helpers/mocks/stripe-webhook-successed.json b/test/helpers/mocks/stripe-webhook-successed.json deleted file mode 100644 index e303a6a6..00000000 --- a/test/helpers/mocks/stripe-webhook-successed.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "id": "evt_1EKvTPAbVbjCtGTWkLZTHEKf", - "object": "event", - "api_version": "2019-03-14", - "created": 1554245575, - "data": { - "object": { - "id": "ch_1EKvTOAbVbjCtGTWjanopixO", - "object": "charge", - "amount": 1001, - "amount_refunded": 0, - "application": null, - "application_fee": null, - "application_fee_amount": null, - "balance_transaction": "txn_1EKvTOAbVbjCtGTWKHLySw60", - "billing_details": { - "address": { - "city": null, - "country": null, - "line1": null, - "line2": null, - "postal_code": null, - "state": null - }, - "email": null, - "name": null, - "phone": null - }, - "captured": true, - "created": 1554245574, - "currency": "usd", - "customer": "cus_EoYaIhhLCiJSzg", - "description": "Feed the cat", - "destination": null, - "dispute": null, - "failure_code": null, - "failure_message": null, - "fraud_details": { - }, - "invoice": null, - "livemode": false, - "metadata": { - "internalId": "{{INTERNAL_ID_PLACEHOLDER}}" - }, - "on_behalf_of": null, - "order": null, - "outcome": { - "network_status": "approved_by_network", - "reason": null, - "risk_level": "normal", - "risk_score": 37, - "seller_message": "Payment complete.", - "type": "authorized" - }, - "paid": true, - "payment_intent": null, - "payment_method_details": { - "card": { - "brand": "mastercard", - "checks": { - "address_line1_check": null, - "address_postal_code_check": null, - "cvc_check": null - }, - "country": "US", - "exp_month": 4, - "exp_year": 2020, - "fingerprint": "fjvOgCIrRn0kxav8", - "funding": "credit", - "last4": "4444", - "three_d_secure": null, - "wallet": null - }, - "type": "card" - }, - "receipt_email": "perchik@cat.com", - "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_1EDc6sAbVbjCtGTW/ch_1EKvTOAbVbjCtGTWjanopixO/rcpt_EoYaOSOSjTK8UikAaT1p62R1vdFsU9G", - "refunded": false, - "refunds": { - "object": "list", - "data": [ - ], - "has_more": false, - "total_count": 0, - "url": "/v1/charges/ch_1EKvTOAbVbjCtGTWjanopixO/refunds" - }, - "review": null, - "shipping": null, - "source": { - "id": "card_1EKvTOAbVbjCtGTWBVRdMh9J", - "object": "card", - "address_city": null, - "address_country": null, - "address_line1": null, - "address_line1_check": null, - "address_line2": null, - "address_state": null, - "address_zip": null, - "address_zip_check": null, - "brand": "MasterCard", - "country": "US", - "customer": "cus_EoYaIhhLCiJSzg", - "cvc_check": null, - "dynamic_last4": null, - "exp_month": 4, - "exp_year": 2020, - "fingerprint": "fjvOgCIrRn0kxav8", - "funding": "credit", - "last4": "4444", - "metadata": { - }, - "name": null, - "tokenization_method": null - }, - "source_transfer": null, - "statement_descriptor": null, - "status": "succeeded", - "transfer_data": null, - "transfer_group": null - } - }, - "livemode": false, - "pending_webhooks": 1, - "request": { - "id": "req_Fs03oiGZiWzNMV", - "idempotency_key": "charge:{{INTERNAL_ID_PLACEHOLDER}}" - }, - "type": "charge.succeeded" -} diff --git a/test/helpers/stripe.js b/test/helpers/stripe.js deleted file mode 100644 index e5407220..00000000 --- a/test/helpers/stripe.js +++ /dev/null @@ -1,14 +0,0 @@ -const crypto = require('crypto'); - -function createSignature(payload, secret) { - const t = Math.floor(Date.now() / 1000); - const v1 = crypto.createHmac('sha256', secret) - .update(`${t}.${payload}`, 'utf8') - .digest('hex'); - - return `t=${t},v1=${v1}`; -} - -module.exports = { - createSignature, -}; diff --git a/yarn.lock b/yarn.lock index 2dc32636..793ebf8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1573,7 +1573,7 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= -"@types/node@*", "@types/node@>= 8", "@types/node@>=8.1.0": +"@types/node@*", "@types/node@>= 8": version "14.0.13" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.13.tgz#ee1128e881b874c371374c1f72201893616417c9" integrity sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA== @@ -8199,7 +8199,7 @@ qrcode-terminal@^0.12.0: resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== -qs@^6.6.0, qs@^6.9.1: +qs@^6.9.1: version "6.9.4" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== @@ -9464,14 +9464,6 @@ strip-url-auth@^1.0.0: resolved "https://registry.yarnpkg.com/strip-url-auth/-/strip-url-auth-1.0.1.tgz#22b0fa3a41385b33be3f331551bbb837fa0cd7ae" integrity sha1-IrD6OkE4WzO+PzMVUbu4N/oM164= -stripe@^8.61.0: - version "8.91.0" - resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.91.0.tgz#771def62f4779491a0152b4665c3694635e6a215" - integrity sha512-qn9cO1tUK/IKpXl2uhVW0G4GPOI5K299qOBH9gDB6vILaqUJhQaNsykzRBtDMLkmLyGh7AFw62ghZPXmlOFdeA== - dependencies: - "@types/node" ">=8.1.0" - qs "^6.6.0" - stubs@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b"