Commit ad06a479 by xuzhenzhao

feat(custom): toolkit

payment
test units
lib add d.ts
parent 6a9ba37f
import { Utils } from '../src/Utils/Utils'
import {PayError, Payment, PayType} from '../src/Payment/Payment'
const payment = new Payment()
const MOCK_GOODS = {
totalAmount: 100,
orderId: 1
}
Utils.setCookie('uid', '130959612')
Utils.setCookie('accessToken', '33441f9c4be74a357b1d416d4aa616eaMjIwNw')
test('test: getBalance', async () => {
const balance = await payment.getBalance()
expect(balance).toBeGreaterThanOrEqual(0)
})
// test('test: computeAmount', () => {
// const {payAmount, payBalance} = payment.computeAmount(100)
// expect(payBalance).toBe(payment.balance)
// })
test('test: 余额支付', async () => {
const res = await payment.toPay({
totalAmount: MOCK_GOODS.totalAmount,
orderId: MOCK_GOODS.orderId,
payType: PayType.BALANCE,
quitUrl: 'https://www.baidu.com',
returnUrl: 'https://www.baidu.com',
redirectUrl: 'https://www.baidu.com'
})
if (res) {
const {errorType, success} = res
expect(errorType === PayError.VALIDATE)
}
})
// test('test: 微信支付')
// test('test: 支付宝支付')
// test('test: 微信 + 余额支付')
// test('test: 支付宝 + 余额支付')
// test('test: createBackInfoUrl')
// test('test: 订单查询')
// test('test: getPayMethodList')
// test('test: getPayChannel')
\ No newline at end of file
import {beforeAll, describe, expect, test} from '@jest/globals'
import {Utils} from '@/Utils/Utils'
import {PayError, Payment, PayType} from '@/Payment/Payment'
import {defaultRequest, DefaultResponse} from "@/Request/Request";
import {ACCESS_TOKEN, UID} from "@/Const";
const globalDatabase: any = {
/**
* 1块钱商品
* https://testnewm.ydl.com/consult/#/pages/jieyou/DownOrder?product_id=88220606058062&uid=130959612&accessToken=33441f9c4be74a357b1d416d4aa616eaMjIwNw&ffrom=m
*/
MOCK_GOODS: {
productId: 88220606058062,
productSpecs: [
{
standardProductSpecId: 255539,
amount: 1
}
],
type: 1,
totalPrice: 1
}
}
const createOrder = async ():Promise<{orderId: number, title: string, payId: number, money: number}> => {
const res = await defaultRequest.post<any, DefaultResponse>('https://testapi.ydl.com/api/consult/user/order/submitWorriesConsultOrder', globalDatabase.MOCK_GOODS)
return res.data
}
beforeAll(async () => {
//
/**
* 测试账号:15706780002 ydl123456
* 请保证账户余额为1.9元
*/
Utils.setCookie(UID, '130959612')
Utils.setCookie(ACCESS_TOKEN, '33441f9c4be74a357b1d416d4aa616eaMjIwNw')
globalDatabase.payment = new Payment()
})
describe('测试用余额购买1块钱商品', () => {
test('test:获取账户余额', async () => {
const balance = await globalDatabase.payment.getBalance()
expect(balance).toBeGreaterThanOrEqual(1)
})
test('test: 测试余额支付', async () => {
const {orderId, money} = await createOrder()
const res = await globalDatabase.payment.toPay({
totalAmount: money,
payType: PayType.BALANCE,
orderId,
redirectUrl: window.encodeURIComponent('https://www.baidu.com'),
returnUrl: 'https://www.baidu.com',
quitUrl: 'https://www.baidu.com'
})
expect(res.success).toBe(true)
})
})
describe('测试用0.9元余额 + 0.1元支付宝金额支付', () => {
test('test:获取账户余额', async () => {
const balance = await globalDatabase.payment.getBalance()
expect(balance).toBe(0.9)
})
test('test: 使用余额+支付宝支付', async () => {
const {orderId, money} = await createOrder()
const res = await globalDatabase.payment.toPay({
totalAmount: money,
payType: PayType.ALIPAY_BALANCE,
orderId,
redirectUrl: window.encodeURIComponent('https://www.baidu.com'),
returnUrl: 'https://www.baidu.com',
quitUrl: 'https://www.baidu.com'
})
expect(res.success).toBe(false)
expect(res.errorType).toBe(PayError.ALIPAY_H5_BREAK)
})
})
describe('测试用0.9元余额 + 0.1元微信金额支付', () => {
test('test:获取账户余额', async () => {
const balance = await globalDatabase.payment.getBalance()
expect(balance).toBe(0.9)
})
test('test: 使用余额+微信支付', async () => {
const {orderId, money} = await createOrder()
const res = await globalDatabase.payment.toPay({
totalAmount: money,
payType: PayType.WECHAT_BALANCE,
orderId,
redirectUrl: window.encodeURIComponent('https://www.baidu.com'),
returnUrl: 'https://www.baidu.com',
quitUrl: 'https://www.baidu.com'
})
expect(res.success).toBe(false)
expect(res.errorType).toBe(PayError.WECHAT_H5_BREAK)
})
})
import {beforeAll, describe, expect, test} from '@jest/globals'
import {Utils} from '@/Utils/Utils'
import {PayError, Payment, PayType} from '@/Payment/Payment'
import {defaultRequest, DefaultResponse} from "@/Request/Request";
import {ACCESS_TOKEN, UID} from "@/Const";
const globalDatabase: any = {
/**
* 1块钱商品
* https://testnewm.ydl.com/consult/#/pages/jieyou/DownOrder?product_id=88220606058062&uid=130959612&accessToken=33441f9c4be74a357b1d416d4aa616eaMjIwNw&ffrom=m
*/
MOCK_GOODS: {
productId: 88220606058062,
productSpecs: [
{
standardProductSpecId: 255539,
amount: 1
}
],
type: 1,
totalPrice: 1
}
}
const createOrder = async (): Promise<{ orderId: number, title: string, payId: number, money: number }> => {
const res = await defaultRequest.post<any, DefaultResponse>('https://testapi.ydl.com/api/consult/user/order/submitWorriesConsultOrder', globalDatabase.MOCK_GOODS)
return res.data
}
beforeAll(async () => {
//
/**
* 测试账号:15706780002 ydl123456
* 请保证账户余额为0元
*/
Utils.setCookie(UID, '130959612')
Utils.setCookie(ACCESS_TOKEN, '33441f9c4be74a357b1d416d4aa616eaMjIwNw')
globalDatabase.payment = new Payment()
})
describe('测试无余额支付情况', () => {
test('test:获取账户余额', async () => {
const balance = await globalDatabase.payment.getBalance()
expect(balance).toBe(0)
})
test('test: 测试余额支付', async () => {
const res = await globalDatabase.payment.toPay({
totalAmount: 1,
payType: PayType.BALANCE,
orderId: globalDatabase.MOCK_GOODS.orderId,
redirectUrl: window.encodeURIComponent('https://www.baidu.com'),
returnUrl: 'https://www.baidu.com',
quitUrl: 'https://www.baidu.com'
})
expect(res.success).toBe(false)
expect(res.errorMessage).toBe('余额不足')
})
})
describe('测试无余额使用支付宝支付', () => {
test('test: 使用支付宝支付', async () => {
const {orderId, money} = await createOrder()
const res = await globalDatabase.payment.toPay({
totalAmount: money,
payType: PayType.ALIPAY,
orderId,
redirectUrl: window.encodeURIComponent('https://www.baidu.com'),
returnUrl: 'https://www.baidu.com',
quitUrl: 'https://www.baidu.com'
})
expect(res.success).toBe(false)
expect(res.errorType).toBe(PayError.ALIPAY_H5_BREAK)
})
})
describe('测试无余额使用微信支付', () => {
test('test: 使用微信支付', async () => {
const {orderId, money} = await createOrder()
const res = await globalDatabase.payment.toPay({
totalAmount: money,
payType: PayType.WECHAT_BALANCE,
orderId,
redirectUrl: window.encodeURIComponent('https://www.baidu.com'),
returnUrl: 'https://www.baidu.com',
quitUrl: 'https://www.baidu.com'
})
expect(res.success).toBe(false)
expect(res.errorType).toBe(PayError.WECHAT_H5_BREAK)
})
})
\ No newline at end of file
import {describe, expect, test} from '@jest/globals'
import {BACK_ORDER_ID, BACK_PAY_ID, PayChannel, PayError, PayErrorMessage, Payment, PayType} from "@/Payment/Payment";
describe('测试: 创建实例', () => {
test('payChannels', () => {
const payment = new Payment({
payChannels: [PayChannel.WX_MWEB, PayChannel.WX_JSAPI]
})
expect(payment.limitPayChannels).toEqual([PayChannel.WX_MWEB, PayChannel.WX_JSAPI])
})
test('no params', () => {
const payment = new Payment()
expect(payment.limitPayChannels).toEqual(Object.keys(PayChannel).map(key => key))
})
})
test('测试订单查询链接', () => {
const testPayId = 111
const testOrderId = 222
const backUrl = Payment.createBackInfoUrl(window.location.href, testPayId, testOrderId)
expect(backUrl).toContain(BACK_PAY_ID)
expect(backUrl).toContain(BACK_ORDER_ID)
expect(backUrl).toContain(testPayId.toString())
expect(backUrl).toContain(testOrderId.toString())
expect(backUrl).toContain(window.location.origin)
})
test('测试价格计算', () => {
const payment = new Payment()
const goodsPrice = 0.3
const balance = 0.1
const {payAmount, payBalance} = payment.computeAmount(goodsPrice, balance)
expect(payAmount).toBe(0.2)
expect(payBalance).toBe(0.1)
})
describe('测试支付入参校验', () => {
const othersParams = {
orderId: 111111111111,
totalAmount: 100,
}
test('ali payType', async () => {
const payment = new Payment({
payChannels: [PayChannel.WX_JSAPI, PayChannel.WX_JSAPI]
})
const {errorMessage, errorType, success} = await payment.toPay({
...othersParams,
payType: PayType.ALIPAY
})
expect(success).toBe(false)
expect(errorType).toBe(PayError.VALIDATE)
expect(errorMessage).toBe(PayErrorMessage.ALIPAY_PAY_CHANNEL_MISSING)
})
test('wechat patType', async () => {
const payment = new Payment({
payChannels: [PayChannel.ALI_WAP]
})
const {errorMessage, errorType, success} = await payment.toPay({
...othersParams,
payType: PayType.WECHAT
})
expect(success).toBe(false)
expect(errorType).toBe(PayError.VALIDATE)
expect(errorMessage).toBe(PayErrorMessage.WECHAT_PAY_CHANNEL_MISSING)
})
test('wechat redirect missing', async () => {
const payment = new Payment({
payChannels: [PayChannel.WX_MWEB, PayChannel.WX_JSAPI]
})
const {errorMessage, errorType, success} = await payment.toPay({
...othersParams,
payType: PayType.WECHAT
})
expect(success).toBe(false)
expect(errorType).toBe(PayError.VALIDATE)
expect(errorMessage).toBe(PayErrorMessage.WECHAT_REDIRECT_URL_MISSING)
})
test('wechat redirect encode', async () => {
const payment = new Payment({
payChannels: [PayChannel.WX_MWEB, PayChannel.WX_JSAPI]
})
const {errorMessage, errorType, success} = await payment.toPay({
...othersParams,
payType: PayType.WECHAT,
redirectUrl: 'https://m.ydl.com'
})
expect(success).toBe(false)
expect(errorType).toBe(PayError.VALIDATE)
expect(errorMessage).toBe(PayErrorMessage.WECHAT_REDIRECT_URL_ENCODE)
})
test('alipay quitUrl missing', async () => {
const payment = new Payment({
payChannels: [PayChannel.ALI_WAP]
})
const {errorMessage, errorType, success} = await payment.toPay({
...othersParams,
payType: PayType.ALIPAY
})
expect(success).toBe(false)
expect(errorType).toBe(PayError.VALIDATE)
expect(errorMessage).toBe(PayErrorMessage.ALIPAY_QUIT_URL_MISSING)
})
test('alipay returnUrl missing', async () => {
const payment = new Payment({
payChannels: [PayChannel.ALI_WAP]
})
const {errorMessage, errorType, success} = await payment.toPay({
...othersParams,
payType: PayType.ALIPAY,
quitUrl: 'https://m.ydl.com'
})
expect(success).toBe(false)
expect(errorType).toBe(PayError.VALIDATE)
expect(errorMessage).toBe(PayErrorMessage.ALIPAY_RETURN_URL_MISSING)
})
})
test('测试微信浏览器支付方式', () => {
const payment = new Payment()
const methods = payment.getPayMethodList(100)
expect(methods.length).toBe(1)
expect(methods[0].value).toBe(PayType.WECHAT)
})
...@@ -2,5 +2,11 @@ ...@@ -2,5 +2,11 @@
module.exports = { module.exports = {
preset: 'ts-jest', preset: 'ts-jest',
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'] testEnvironmentOptions: {
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.3 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1 wechatdevtools/1.06.2208010 MicroMessenger/8.0.5 Language/zh_CN webview/16601963014137226 webdebugger port/60125 token/0e6423f01c6f63d782b58ad418b07059'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
}; };
\ No newline at end of file
...@@ -2,46 +2,53 @@ ...@@ -2,46 +2,53 @@
"name": "@ydl-packages/toolkit", "name": "@ydl-packages/toolkit",
"version": "1.0.1-next.2", "version": "1.0.1-next.2",
"description": "", "description": "",
"main": "./dist/index.umd.js", "main": "./dist/index.mjs",
"scripts": { "scripts": {
"lib": "npx vite build" "lib": "npx vite build",
"test": "npx jest __tests__"
}, },
"keywords": [], "keywords": [
"author": "", "pay",
"toolkit",
"utils"
],
"author": "xuzhenzhao",
"license": "ISC", "license": "ISC",
"type": "module",
"files": [ "files": [
"dist" "dist"
], ],
"module": "./dist/index.js", "module": "./dist/index.mjs",
"exports": { "exports": {
".": { ".": {
"import": "./dist/index.js", "import": "./dist/index.mjs",
"require": "./dist/index.umd.cjs" "require": "./dist/index.umd.js"
} }
}, },
"types": "'./dist/index.d.ts",
"dependencies": { "dependencies": {
"@types/consola": "^2.2.5",
"axios": "^0.27.2", "axios": "^0.27.2",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"consola": "^2.15.3", "consola": "^2.15.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"mathjs": "^11.0.1", "mathjs": "^11.0.1",
"qs": "^6.11.0", "qs": "^6.11.0",
"regenerator-runtime": "^0.13.9", "regenerator-runtime": "^0.13.9"
"ts-jest": "^28.0.7",
"@types/jest": "^28.1.6"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^28.1.3",
"@types/consola": "^2.2.5",
"@types/jest": "^28.1.6",
"ts-jest": "^28.0.7",
"vite-plugin-dts": "^1.4.1",
"@types/blueimp-md5": "^2.18.0", "@types/blueimp-md5": "^2.18.0",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@types/node": "^18.6.2", "@types/node": "^18.6.2",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"typedoc": "^0.23.9",
"typedoc-theme-hierarchy": "^3.0.0",
"typescript": "^4.7.4",
"jest": "^28.1.3", "jest": "^28.1.3",
"jest-environment-jsdom": "^28.1.3", "jest-environment-jsdom": "^28.1.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1",
"typedoc": "^0.23.9",
"typedoc-theme-hierarchy": "^3.0.0",
"typescript": "^4.7.4"
} }
} }
...@@ -15,7 +15,11 @@ export type BackInfo = { ...@@ -15,7 +15,11 @@ export type BackInfo = {
} }
export type OrderStateCallBack = (params: BackInfo & { isPay: boolean }) => void export type OrderStateCallBack = (params: BackInfo & { isPay: boolean }) => void
export const enum PayType { export type PaymentParams = {
payChannels?: PayChannel[]
}
export enum PayType {
/** /**
* 余额支付 * 余额支付
*/ */
...@@ -55,7 +59,10 @@ export const enum PayType { ...@@ -55,7 +59,10 @@ export const enum PayType {
} }
export const enum PayChannel { /**
* 余额支付方式默认支持, 无需声明
*/
export enum PayChannel {
WX_MWEB = 'WX_MWEB', WX_MWEB = 'WX_MWEB',
WX_JSAPI = 'WX_JSAPI', WX_JSAPI = 'WX_JSAPI',
WX_NATIVE = 'WX_NATIVE', WX_NATIVE = 'WX_NATIVE',
...@@ -75,30 +82,46 @@ export type ToPayParams = { ...@@ -75,30 +82,46 @@ export type ToPayParams = {
* 支付宝h5支付失败回调地址 * 支付宝h5支付失败回调地址
* doc: https://opendocs.alipay.com/open/203/105285#%E9%87%8D%E8%A6%81%E5%85%A5%E5%8F%82%E8%AF%B4%E6%98%8E_3 * doc: https://opendocs.alipay.com/open/203/105285#%E9%87%8D%E8%A6%81%E5%85%A5%E5%8F%82%E8%AF%B4%E6%98%8E_3
*/ */
quitUrl: string; quitUrl?: string;
/** /**
* 支付宝h5支付成功回调地址 * 支付宝h5支付成功回调地址
* doc: https://opendocs.alipay.com/open/203/105285#%E9%87%8D%E8%A6%81%E5%85%A5%E5%8F%82%E8%AF%B4%E6%98%8E_3 * doc: https://opendocs.alipay.com/open/203/105285#%E9%87%8D%E8%A6%81%E5%85%A5%E5%8F%82%E8%AF%B4%E6%98%8E_3
*/ */
returnUrl: string; returnUrl?: string;
/** /**
* 微信h5支付回调地址 * 微信h5支付回调地址
* doc: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/combine/chapter10_1.shtml * doc: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/combine/chapter10_1.shtml
*/ */
redirectUrl: string redirectUrl?: string
} }
export enum PayError { export enum PayError {
WECHAT_JSSDK = "WECHAT_JSSDK", WECHAT_JSSDK = "WECHAT_JSSDK",
BALANCE = 'BALANCE', BALANCE = 'BALANCE',
VALIDATE = 'VALIDATE', VALIDATE = 'VALIDATE',
BACKEND = 'BACKEND' BACKEND = 'BACKEND',
WECHAT_H5_BREAK = 'WECHAT_H5_BREAK',
ALIPAY_H5_BREAK = 'ALIPAY_H5_BREAK',
UNKNOWN = 'UNKNOWN'
}
export enum PayErrorMessage {
BALANCE_EMPTY = '余额不足',
WECHAT_REDIRECT_URL_MISSING= `请填写微信回调地址页面 'redirectUrl`,
WECHAT_REDIRECT_URL_ENCODE = 'redirectUrl需要encode',
WECHAT_PAY_CHANNEL_MISSING =`缺少持的渠道参数:'WX_MWEB','WX_JSAPI'`,
ALIPAY_QUIT_URL_MISSING = `缺少支付失败回调地址:'quitUrl'`,
ALIPAY_RETURN_URL_MISSING = `缺少支付成功回调地址:'returnUrl'`,
ALIPAY_PAY_CHANNEL_MISSING = `缺少持的渠道参数:'ALI_WAP'`,
WECHAT_JSSDK_PAY_ERROR = '微信JSSDK支付失败',
WECHAT_H5_PAY_BREAK = '微信h5支付中断',
ALIPAY_H5_PAY_BREAK = '支付宝支付中断'
} }
export type ToPayReturns = { export type ToPayReturns = {
errorMessage?: string errorMessage?: PayErrorMessage
errorType: PayError errorType: PayError
success: boolean success: boolean
redirectUrl?: string
} }
export interface DoUnifiedParams extends Omit<ToPayParams, 'totalAmount' | 'redirectUrl'> { export interface DoUnifiedParams extends Omit<ToPayParams, 'totalAmount' | 'redirectUrl'> {
......
import axios, { Axios, AxiosRequestConfig } from 'axios' import axios, {Axios, AxiosRequestConfig} from 'axios'
import { ACCESS_TOKEN, UID } from '../Const' import {ACCESS_TOKEN, UID} from '../Const'
import { Utils } from '../Utils/Utils' import {Utils} from '../Utils/Utils'
export const createRequest = axios.create export const createRequest = axios.create
...@@ -14,4 +14,13 @@ defaultRequest.interceptors.request.use((config) => { ...@@ -14,4 +14,13 @@ defaultRequest.interceptors.request.use((config) => {
}, },
} }
}) })
export { defaultRequest } defaultRequest.interceptors.response.use((response) => {
\ No newline at end of file return response.data
}, err => Promise.reject(err))
export {defaultRequest}
export type DefaultResponse = {
code: string
data: any
msg: string
}
\ No newline at end of file
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "es2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */ /* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
......
import path from 'path' import path from 'path'
import { defineConfig } from 'vite' import {defineConfig} from 'vite'
import dts from 'vite-plugin-dts'
export default defineConfig(() => { export default defineConfig(() => {
return { return {
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
build: { build: {
target: "es2015", target: "es2015",
lib: { lib: {
entry: path.resolve(__dirname, 'src/index'), entry: path.resolve(__dirname, 'src/index'),
name: 'toolkit', name: 'toolkit',
...@@ -13,6 +21,7 @@ export default defineConfig(() => { ...@@ -13,6 +21,7 @@ export default defineConfig(() => {
sourcemap: 'inline' sourcemap: 'inline'
}, },
outDir: path.resolve(__dirname, 'dist') outDir: path.resolve(__dirname, 'dist')
} },
plugins: [dts()]
} }
}) })
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment