mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Compare commits
46 Commits
fiz/item-c
...
qa/frog
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
118840d6ce | ||
|
|
bd46740d0e | ||
|
|
f1a1bc0c6f | ||
|
|
d2ddab11e5 | ||
|
|
d76ed9eaa9 | ||
|
|
0677c4ab9c | ||
|
|
faa1db4687 | ||
|
|
92b4c10ed2 | ||
|
|
58ec028d5d | ||
|
|
02d4d2dd95 | ||
|
|
375ca3e441 | ||
|
|
0e8ca110d3 | ||
|
|
ef66387c45 | ||
|
|
2e4c0e1fb9 | ||
|
|
5dbd10e53e | ||
|
|
5ba6d24503 | ||
|
|
6409c4c958 | ||
|
|
91ccba9e8a | ||
|
|
18239b7828 | ||
|
|
72200060fa | ||
|
|
af3c37a059 | ||
|
|
c00aaec8e9 | ||
|
|
ba99a65bd4 | ||
|
|
71c2e19330 | ||
|
|
9b52198631 | ||
|
|
f4f964bfd8 | ||
|
|
f33e256b57 | ||
|
|
0a6f138de8 | ||
|
|
bc77c7698f | ||
|
|
1aba2be57f | ||
|
|
7dd3ca485a | ||
|
|
b9597319a3 | ||
|
|
0e99142283 | ||
|
|
a3b3b281a4 | ||
|
|
787f64a8e5 | ||
|
|
625db6bc4e | ||
|
|
fcb1d06c7d | ||
|
|
cb2b837e2f | ||
|
|
7c81b71c0c | ||
|
|
b2b2d0fac6 | ||
|
|
aa8af15cc6 | ||
|
|
eede13f100 | ||
|
|
5540c6e187 | ||
|
|
6ebf3d0d03 | ||
|
|
6b8e6ed7a1 | ||
|
|
b00f463db5 |
@@ -150,7 +150,7 @@ describe('emails', () => {
|
|||||||
|
|
||||||
sendTxn(mailingInfo, emailType);
|
sendTxn(mailingInfo, emailType);
|
||||||
expect(got.post).to.be.called;
|
expect(got.post).to.be.called;
|
||||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
emailType: sinon.match.same(emailType),
|
emailType: sinon.match.same(emailType),
|
||||||
@@ -234,7 +234,7 @@ describe('emails', () => {
|
|||||||
|
|
||||||
sendTxn(mailingInfo, emailType);
|
sendTxn(mailingInfo, emailType);
|
||||||
expect(got.post).to.be.called;
|
expect(got.post).to.be.called;
|
||||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
emailType: sinon.match.same(emailType),
|
emailType: sinon.match.same(emailType),
|
||||||
@@ -254,7 +254,7 @@ describe('emails', () => {
|
|||||||
|
|
||||||
sendTxn(mailingInfo, emailType, variables);
|
sendTxn(mailingInfo, emailType, variables);
|
||||||
expect(got.post).to.be.called;
|
expect(got.post).to.be.called;
|
||||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||||
|
|||||||
@@ -12,11 +12,33 @@ const { i18n } = common;
|
|||||||
describe('Apple Payments', () => {
|
describe('Apple Payments', () => {
|
||||||
const subKey = 'basic_3mo';
|
const subKey = 'basic_3mo';
|
||||||
|
|
||||||
|
let iapSetupStub;
|
||||||
|
let iapValidateStub;
|
||||||
|
let iapIsValidatedStub;
|
||||||
|
let iapIsCanceledStub;
|
||||||
|
let iapIsExpiredStub;
|
||||||
|
let paymentBuySkuStub;
|
||||||
|
let iapGetPurchaseDataStub;
|
||||||
|
let validateGiftMessageStub;
|
||||||
|
let paymentsCreateSubscritionStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
iapSetupStub = sinon.stub(iap, 'setup').resolves();
|
||||||
|
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
iap.setup.restore();
|
||||||
|
iap.validate.restore();
|
||||||
|
iap.isValidated.restore();
|
||||||
|
iap.isExpired.restore();
|
||||||
|
iap.isCanceled.restore();
|
||||||
|
iap.getPurchaseData.restore();
|
||||||
|
});
|
||||||
|
|
||||||
describe('verifyPurchase', () => {
|
describe('verifyPurchase', () => {
|
||||||
let sku; let user; let token; let receipt; let
|
let sku; let user; let token; let receipt; let
|
||||||
headers;
|
headers;
|
||||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
|
|
||||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
token = 'testToken';
|
token = 'testToken';
|
||||||
@@ -25,13 +47,9 @@ describe('Apple Payments', () => {
|
|||||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||||
headers = {};
|
headers = {};
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
|
||||||
.resolves();
|
|
||||||
iapValidateStub = sinon.stub(iap, 'validate')
|
|
||||||
.resolves({});
|
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||||
sinon.stub(iap, 'isExpired').returns(false);
|
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||||
sinon.stub(iap, 'isCanceled').returns(false);
|
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||||
@@ -42,12 +60,6 @@ describe('Apple Payments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
iap.setup.restore();
|
|
||||||
iap.validate.restore();
|
|
||||||
iap.isValidated.restore();
|
|
||||||
iap.isExpired.restore();
|
|
||||||
iap.isCanceled.restore();
|
|
||||||
iap.getPurchaseData.restore();
|
|
||||||
payments.buySkuItem.restore();
|
payments.buySkuItem.restore();
|
||||||
gems.validateGiftMessage.restore();
|
gems.validateGiftMessage.restore();
|
||||||
});
|
});
|
||||||
@@ -209,9 +221,6 @@ describe('Apple Payments', () => {
|
|||||||
describe('subscribe', () => {
|
describe('subscribe', () => {
|
||||||
let sub; let sku; let user; let token; let receipt; let headers; let
|
let sub; let sku; let user; let token; let receipt; let headers; let
|
||||||
nextPaymentProcessing;
|
nextPaymentProcessing;
|
||||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
|
|
||||||
let paymentsCreateSubscritionStub; let
|
|
||||||
iapGetPurchaseDataStub;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sub = common.content.subscriptionBlocks[subKey];
|
sub = common.content.subscriptionBlocks[subKey];
|
||||||
@@ -223,12 +232,10 @@ describe('Apple Payments', () => {
|
|||||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||||
user = new User();
|
user = new User();
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
|
||||||
.resolves();
|
|
||||||
iapValidateStub = sinon.stub(iap, 'validate')
|
|
||||||
.resolves({});
|
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||||
.returns(true);
|
.returns(true);
|
||||||
|
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||||
|
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{
|
.returns([{
|
||||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||||
@@ -250,10 +257,6 @@ describe('Apple Payments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
iap.setup.restore();
|
|
||||||
iap.validate.restore();
|
|
||||||
iap.isValidated.restore();
|
|
||||||
iap.getPurchaseData.restore();
|
|
||||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -270,6 +273,29 @@ describe('Apple Payments', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error if no active subscription is found', async () => {
|
||||||
|
iap.isCanceled.restore();
|
||||||
|
iapIsCanceledStub = sinon.stub(iap, 'isCanceled')
|
||||||
|
.returns(true);
|
||||||
|
|
||||||
|
iap.getPurchaseData.restore();
|
||||||
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
|
.returns([{
|
||||||
|
expirationDate: moment.utc().add({ day: -2 }).toDate(),
|
||||||
|
purchaseDate: new Date(),
|
||||||
|
productId: 'subscription1month',
|
||||||
|
transactionId: token,
|
||||||
|
originalTransactionId: token,
|
||||||
|
}]);
|
||||||
|
|
||||||
|
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
httpCode: 401,
|
||||||
|
name: 'NotAuthorized',
|
||||||
|
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const subOptions = [
|
const subOptions = [
|
||||||
{
|
{
|
||||||
sku: 'subscription1month',
|
sku: 'subscription1month',
|
||||||
@@ -574,8 +600,7 @@ describe('Apple Payments', () => {
|
|||||||
describe('cancelSubscribe ', () => {
|
describe('cancelSubscribe ', () => {
|
||||||
let user; let token; let receipt; let headers; let customerId; let
|
let user; let token; let receipt; let headers; let customerId; let
|
||||||
expirationDate;
|
expirationDate;
|
||||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
let paymentCancelSubscriptionSpy;
|
||||||
paymentCancelSubscriptionSpy;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
token = 'test-token';
|
token = 'test-token';
|
||||||
@@ -584,8 +609,7 @@ describe('Apple Payments', () => {
|
|||||||
customerId = 'test-customerId';
|
customerId = 'test-customerId';
|
||||||
expirationDate = moment.utc();
|
expirationDate = moment.utc();
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
iapValidateStub.restore();
|
||||||
.resolves();
|
|
||||||
iapValidateStub = sinon.stub(iap, 'validate')
|
iapValidateStub = sinon.stub(iap, 'validate')
|
||||||
.resolves({
|
.resolves({
|
||||||
expirationDate,
|
expirationDate,
|
||||||
@@ -593,8 +617,8 @@ describe('Apple Payments', () => {
|
|||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||||
sinon.stub(iap, 'isCanceled').returns(false);
|
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||||
sinon.stub(iap, 'isExpired').returns(true);
|
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
|
||||||
user = new User();
|
user = new User();
|
||||||
user.profile.name = 'sender';
|
user.profile.name = 'sender';
|
||||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||||
@@ -606,13 +630,7 @@ describe('Apple Payments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
iap.setup.restore();
|
paymentCancelSubscriptionSpy.restore();
|
||||||
iap.validate.restore();
|
|
||||||
iap.isValidated.restore();
|
|
||||||
iap.isExpired.restore();
|
|
||||||
iap.isCanceled.restore();
|
|
||||||
iap.getPurchaseData.restore();
|
|
||||||
payments.cancelSubscription.restore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if we are missing a subscription', async () => {
|
it('should throw an error if we are missing a subscription', async () => {
|
||||||
@@ -695,6 +713,8 @@ describe('Apple Payments', () => {
|
|||||||
expect(iapIsValidatedStub).to.be.calledWith({
|
expect(iapIsValidatedStub).to.be.calledWith({
|
||||||
expirationDate,
|
expirationDate,
|
||||||
});
|
});
|
||||||
|
expect(iapIsCanceledStub).to.be.calledOnce;
|
||||||
|
expect(iapIsExpiredStub).to.be.calledOnce;
|
||||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||||
|
|
||||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||||
|
|||||||
@@ -11,12 +11,36 @@ const { i18n } = common;
|
|||||||
|
|
||||||
describe('Google Payments', () => {
|
describe('Google Payments', () => {
|
||||||
const subKey = 'basic_3mo';
|
const subKey = 'basic_3mo';
|
||||||
|
let iapSetupStub;
|
||||||
|
let iapValidateStub;
|
||||||
|
let iapIsValidatedStub;
|
||||||
|
let paymentBuySkuStub;
|
||||||
|
let validateGiftMessageStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
iapSetupStub = sinon.stub(iap, 'setup')
|
||||||
|
.resolves();
|
||||||
|
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||||
|
.returns(true);
|
||||||
|
sinon.stub(iap, 'isCanceled').returns(false);
|
||||||
|
sinon.stub(iap, 'isExpired').returns(false);
|
||||||
|
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||||
|
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
iap.setup.restore();
|
||||||
|
iap.validate.restore();
|
||||||
|
iap.isValidated.restore();
|
||||||
|
iap.isCanceled.restore();
|
||||||
|
iap.isExpired.restore();
|
||||||
|
payments.buySkuItem.restore();
|
||||||
|
gems.validateGiftMessage.restore();
|
||||||
|
});
|
||||||
|
|
||||||
describe('verifyPurchase', () => {
|
describe('verifyPurchase', () => {
|
||||||
let sku; let user; let token; let receipt; let signature; let
|
let sku; let user; let token; let receipt; let signature; let
|
||||||
headers;
|
headers;
|
||||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
|
||||||
paymentBuySkuStub; let validateGiftMessageStub;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||||
@@ -25,21 +49,7 @@ describe('Google Payments', () => {
|
|||||||
signature = '';
|
signature = '';
|
||||||
headers = {};
|
headers = {};
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
|
||||||
.resolves();
|
|
||||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
|
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
|
||||||
.returns(true);
|
|
||||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
|
||||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
iap.setup.restore();
|
|
||||||
iap.validate.restore();
|
|
||||||
iap.isValidated.restore();
|
|
||||||
payments.buySkuItem.restore();
|
|
||||||
gems.validateGiftMessage.restore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if receipt is invalid', async () => {
|
it('should throw an error if receipt is invalid', async () => {
|
||||||
@@ -160,8 +170,7 @@ describe('Google Payments', () => {
|
|||||||
describe('subscribe', () => {
|
describe('subscribe', () => {
|
||||||
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
|
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
|
||||||
nextPaymentProcessing;
|
nextPaymentProcessing;
|
||||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
let paymentsCreateSubscritionStub;
|
||||||
paymentsCreateSubscritionStub;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sub = common.content.subscriptionBlocks[subKey];
|
sub = common.content.subscriptionBlocks[subKey];
|
||||||
@@ -173,19 +182,12 @@ describe('Google Payments', () => {
|
|||||||
signature = '';
|
signature = '';
|
||||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
|
||||||
.resolves();
|
|
||||||
iapValidateStub = sinon.stub(iap, 'validate')
|
iapValidateStub = sinon.stub(iap, 'validate')
|
||||||
.resolves({});
|
.resolves({});
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
|
||||||
.returns(true);
|
|
||||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
|
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
iap.setup.restore();
|
|
||||||
iap.validate.restore();
|
|
||||||
iap.isValidated.restore();
|
|
||||||
payments.createSubscription.restore();
|
payments.createSubscription.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -243,7 +245,7 @@ describe('Google Payments', () => {
|
|||||||
describe('cancelSubscribe ', () => {
|
describe('cancelSubscribe ', () => {
|
||||||
let user; let token; let receipt; let signature; let headers; let customerId; let
|
let user; let token; let receipt; let signature; let headers; let customerId; let
|
||||||
expirationDate;
|
expirationDate;
|
||||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
let iapGetPurchaseDataStub; let
|
||||||
paymentCancelSubscriptionSpy;
|
paymentCancelSubscriptionSpy;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -253,17 +255,12 @@ describe('Google Payments', () => {
|
|||||||
signature = '';
|
signature = '';
|
||||||
customerId = 'test-customerId';
|
customerId = 'test-customerId';
|
||||||
expirationDate = moment.utc();
|
expirationDate = moment.utc();
|
||||||
|
|
||||||
iapSetupStub = sinon.stub(iap, 'setup')
|
|
||||||
.resolves();
|
|
||||||
iapValidateStub = sinon.stub(iap, 'validate')
|
iapValidateStub = sinon.stub(iap, 'validate')
|
||||||
.resolves({
|
.resolves({
|
||||||
expirationDate,
|
expirationDate,
|
||||||
});
|
});
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
|
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
|
||||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
|
||||||
.returns(true);
|
|
||||||
|
|
||||||
user = new User();
|
user = new User();
|
||||||
user.profile.name = 'sender';
|
user.profile.name = 'sender';
|
||||||
@@ -276,9 +273,6 @@ describe('Google Payments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
iap.setup.restore();
|
|
||||||
iap.validate.restore();
|
|
||||||
iap.isValidated.restore();
|
|
||||||
iap.getPurchaseData.restore();
|
iap.getPurchaseData.restore();
|
||||||
payments.cancelSubscription.restore();
|
payments.cancelSubscription.restore();
|
||||||
});
|
});
|
||||||
@@ -308,6 +302,8 @@ describe('Google Payments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel a user subscription', async () => {
|
it('should cancel a user subscription', async () => {
|
||||||
|
iap.isCanceled.restore();
|
||||||
|
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||||
await googlePayments.cancelSubscribe(user, headers);
|
await googlePayments.cancelSubscribe(user, headers);
|
||||||
|
|
||||||
expect(iapSetupStub).to.be.calledOnce;
|
expect(iapSetupStub).to.be.calledOnce;
|
||||||
@@ -332,11 +328,20 @@ describe('Google Payments', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
|
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
|
||||||
|
iap.isCanceled.restore();
|
||||||
|
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||||
const laterDate = moment.utc().add(7, 'days');
|
const laterDate = moment.utc().add(7, 'days');
|
||||||
iap.getPurchaseData.restore();
|
iap.getPurchaseData.restore();
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{ expirationDate, autoRenewing: false },
|
.returns([{
|
||||||
{ expirationDate: laterDate, autoRenewing: false },
|
startTimeMillis: expirationDate.valueOf(),
|
||||||
|
expirationDate,
|
||||||
|
autoRenewing: false,
|
||||||
|
}, {
|
||||||
|
startTimeMillis: laterDate.valueOf(),
|
||||||
|
expirationDate: laterDate,
|
||||||
|
autoRenewing: false,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
await googlePayments.cancelSubscribe(user, headers);
|
await googlePayments.cancelSubscribe(user, headers);
|
||||||
|
|
||||||
@@ -365,7 +370,12 @@ describe('Google Payments', () => {
|
|||||||
iap.getPurchaseData.restore();
|
iap.getPurchaseData.restore();
|
||||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||||
.returns([{ autoRenewing: true }]);
|
.returns([{ autoRenewing: true }]);
|
||||||
await googlePayments.cancelSubscribe(user, headers);
|
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
httpCode: 401,
|
||||||
|
name: 'NotAuthorized',
|
||||||
|
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||||
|
});
|
||||||
|
|
||||||
expect(iapSetupStub).to.be.calledOnce;
|
expect(iapSetupStub).to.be.calledOnce;
|
||||||
expect(iapValidateStub).to.be.calledOnce;
|
expect(iapValidateStub).to.be.calledOnce;
|
||||||
@@ -388,8 +398,12 @@ describe('Google Payments', () => {
|
|||||||
.returns([{ expirationDate, autoRenewing: false },
|
.returns([{ expirationDate, autoRenewing: false },
|
||||||
{ autoRenewing: true },
|
{ autoRenewing: true },
|
||||||
{ expirationDate, autoRenewing: false }]);
|
{ expirationDate, autoRenewing: false }]);
|
||||||
await googlePayments.cancelSubscribe(user, headers);
|
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
httpCode: 401,
|
||||||
|
name: 'NotAuthorized',
|
||||||
|
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||||
|
});
|
||||||
expect(iapSetupStub).to.be.calledOnce;
|
expect(iapSetupStub).to.be.calledOnce;
|
||||||
expect(iapValidateStub).to.be.calledOnce;
|
expect(iapValidateStub).to.be.calledOnce;
|
||||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
userIdentifier () {
|
userIdentifier () {
|
||||||
this.isSearching = true;
|
this.isSearching = true;
|
||||||
this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
|
this.$store.dispatch('admin:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
|
||||||
this.isSearching = false;
|
this.isSearching = false;
|
||||||
if (users.length === 1) {
|
if (users.length === 1) {
|
||||||
this.loadUser(users[0]._id);
|
this.loadUser(users[0]._id);
|
||||||
|
|||||||
@@ -5,6 +5,19 @@
|
|||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div class="form col-12">
|
<div class="form col-12">
|
||||||
|
<div class="btn-group float-right">
|
||||||
|
<button
|
||||||
|
class="btn btn-danger"
|
||||||
|
@click="confirmDeleteHero"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-once
|
||||||
|
class="svg-icon icon-16 mt-1 mb-1"
|
||||||
|
v-html="icons.deleteIcon"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<basic-details
|
<basic-details
|
||||||
:user-id="hero._id"
|
:user-id="hero._id"
|
||||||
:auth="hero.auth"
|
:auth="hero.auth"
|
||||||
@@ -96,6 +109,53 @@
|
|||||||
:reset-counter="resetCounter"
|
:reset-counter="resetCounter"
|
||||||
@clear-data="clearData"
|
@clear-data="clearData"
|
||||||
/>
|
/>
|
||||||
|
<b-modal
|
||||||
|
id="delete-member-modal"
|
||||||
|
title="Delete Member"
|
||||||
|
ok-title="Delete"
|
||||||
|
ok-variant="danger"
|
||||||
|
cancel-title="Cancel"
|
||||||
|
@ok="deleteHero"
|
||||||
|
>
|
||||||
|
<b-modal-body>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to delete this member?
|
||||||
|
</p>
|
||||||
|
<p class="errorMessage">
|
||||||
|
Please note: This action cannot be undone!
|
||||||
|
</p>
|
||||||
|
<div class="ml-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="deleteAccountCheck"
|
||||||
|
v-model="deleteHabiticaAccount"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="deleteAccountCheck"
|
||||||
|
>
|
||||||
|
Delete Habitica account
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="deleteAmplitudeCheck"
|
||||||
|
v-model="deleteAmplitudeData"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="form-check-label"
|
||||||
|
for="deleteAmplitudeCheck"
|
||||||
|
>
|
||||||
|
Delete Amplitude data
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-modal-body>
|
||||||
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,6 +208,7 @@ import CustomizationsOwned from './customizationsOwned.vue';
|
|||||||
import Achievements from './achievements.vue';
|
import Achievements from './achievements.vue';
|
||||||
import UserHistory from './userHistory.vue';
|
import UserHistory from './userHistory.vue';
|
||||||
import Stats from './stats.vue';
|
import Stats from './stats.vue';
|
||||||
|
import deleteIcon from '@/assets/svg/delete.svg?raw';
|
||||||
|
|
||||||
import { userStateMixin } from '../../../../mixins/userState';
|
import { userStateMixin } from '../../../../mixins/userState';
|
||||||
|
|
||||||
@@ -184,6 +245,11 @@ export default {
|
|||||||
hasParty: false,
|
hasParty: false,
|
||||||
partyNotExistError: false,
|
partyNotExistError: false,
|
||||||
adminHasPrivForParty: true,
|
adminHasPrivForParty: true,
|
||||||
|
deleteHabiticaAccount: true,
|
||||||
|
deleteAmplitudeData: true,
|
||||||
|
icons: Object.freeze({
|
||||||
|
deleteIcon,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -249,6 +315,25 @@ export default {
|
|||||||
|
|
||||||
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
||||||
},
|
},
|
||||||
|
confirmDeleteHero () {
|
||||||
|
if (this.hero._id === this.user._id) {
|
||||||
|
window.alert('You cannot delete your own account.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$root.$emit('bv::show::modal', 'delete-member-modal');
|
||||||
|
},
|
||||||
|
deleteHero () {
|
||||||
|
this.$store.dispatch('hall:deleteHero', {
|
||||||
|
uuid: this.hero._id,
|
||||||
|
deleteHabiticaAccount: this.deleteHabiticaAccount,
|
||||||
|
deleteAmplitudeData: this.deleteAmplitudeData,
|
||||||
|
}).then(() => {
|
||||||
|
this.$root.$emit('bv::hide::modal', 'delete-member-modal');
|
||||||
|
this.$router.push({ name: 'adminPanel' });
|
||||||
|
}).catch(err => {
|
||||||
|
window.alert(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
hasUnsavedChanges (...comparisons) {
|
hasUnsavedChanges (...comparisons) {
|
||||||
for (const index in comparisons) {
|
for (const index in comparisons) {
|
||||||
if (index && comparisons[index]) {
|
if (index && comparisons[index]) {
|
||||||
|
|||||||
@@ -22,9 +22,6 @@
|
|||||||
>
|
>
|
||||||
<p v-if="partyNotExistError">
|
<p v-if="partyNotExistError">
|
||||||
ERROR: User has a Party ID but that Party does not exist.
|
ERROR: User has a Party ID but that Party does not exist.
|
||||||
If you are seeing a red error notification on screen now
|
|
||||||
("<strong>Group with id ... not found</strong>"), it's refering to this issue.
|
|
||||||
<br>Ask a database admin to delete the user's Party ID ({{ userPartyData._id }}).
|
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="questErrors"
|
v-if="questErrors"
|
||||||
@@ -37,7 +34,11 @@
|
|||||||
Party ID
|
Party ID
|
||||||
</label>
|
</label>
|
||||||
<strong class="col-sm-9 col-form-label">
|
<strong class="col-sm-9 col-form-label">
|
||||||
{{ groupPartyData._id }}
|
<router-link
|
||||||
|
:to="{'name': 'groupAdminGroup', 'params': {'groupId': groupPartyData._id}}"
|
||||||
|
>
|
||||||
|
{{ groupPartyData._id }}
|
||||||
|
</router-link>
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
@@ -63,6 +64,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!userIsPartyLeader"
|
||||||
|
class="btn btn-warning mr-2"
|
||||||
|
@click="makePartyLeader()"
|
||||||
|
>
|
||||||
|
Make Party Leader
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
@click="removeFromParty()"
|
@click="removeFromParty()"
|
||||||
@@ -284,8 +292,6 @@ function resetData (self) {
|
|||||||
|
|
||||||
if (self.partyNotExistError) {
|
if (self.partyNotExistError) {
|
||||||
self.errorsOrWarningsExist = true;
|
self.errorsOrWarningsExist = true;
|
||||||
} else {
|
|
||||||
self.userIsPartyLeader = self.groupPartyData.leader === self.userId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for quest errors even if party doesn't exist (user can have old quest data)
|
// check for quest errors even if party doesn't exist (user can have old quest data)
|
||||||
@@ -329,13 +335,17 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
userIsPartyLeader: false,
|
|
||||||
questStatus: '',
|
questStatus: '',
|
||||||
questErrors: '',
|
questErrors: '',
|
||||||
errorsOrWarningsExist: false,
|
errorsOrWarningsExist: false,
|
||||||
expand: false,
|
expand: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
userIsPartyLeader () {
|
||||||
|
return this.groupPartyData.leader === this.userId;
|
||||||
|
},
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
resetCounter () {
|
resetCounter () {
|
||||||
resetData(this);
|
resetData(this);
|
||||||
@@ -352,6 +362,14 @@ export default {
|
|||||||
reloadData: true,
|
reloadData: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async makePartyLeader () {
|
||||||
|
await this.$store.dispatch('guilds:update', {
|
||||||
|
group: {
|
||||||
|
id: this.groupPartyData._id,
|
||||||
|
leader: this.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,6 +15,25 @@
|
|||||||
:class="{ 'open': expand }"
|
:class="{ 'open': expand }"
|
||||||
>
|
>
|
||||||
Subscription, Monthly Perks
|
Subscription, Monthly Perks
|
||||||
|
<span
|
||||||
|
v-if="isSubscribed() && !isCancelled()"
|
||||||
|
class="text-success float-right ml-3"
|
||||||
|
>
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="isSubscribed() && isCancelled()"
|
||||||
|
class="text-success float-right ml-3"
|
||||||
|
>
|
||||||
|
Active until {{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="hero.purchased.plan.customerId && hero.purchased.plan.dateTerminated"
|
||||||
|
class="text-warning float-right ml-3"
|
||||||
|
>
|
||||||
|
Inactive
|
||||||
|
</span>
|
||||||
|
|
||||||
<b
|
<b
|
||||||
v-if="hasUnsavedChanges && !expand"
|
v-if="hasUnsavedChanges && !expand"
|
||||||
class="text-warning float-right"
|
class="text-warning float-right"
|
||||||
@@ -46,7 +65,7 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
>
|
||||||
<option value="groupPlan">
|
<option value="Group Plan">
|
||||||
Group Plan
|
Group Plan
|
||||||
</option>
|
</option>
|
||||||
<option value="Stripe">
|
<option value="Stripe">
|
||||||
@@ -116,20 +135,10 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<formRow
|
||||||
class="form-group row"
|
v-model="hero.purchased.plan.customerId"
|
||||||
>
|
label="Customer ID"
|
||||||
<label class="col-sm-3 col-form-label">
|
/>
|
||||||
Customer ID:
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<input
|
|
||||||
v-model="hero.purchased.plan.customerId"
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="hero.purchased.plan.planId === 'group_plan_auto'"
|
v-if="hero.purchased.plan.planId === 'group_plan_auto'"
|
||||||
class="form-group row"
|
class="form-group row"
|
||||||
@@ -154,7 +163,11 @@
|
|||||||
>
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h6 class="card-title">
|
<h6 class="card-title">
|
||||||
{{ group.name }}
|
<router-link
|
||||||
|
:to="{ name: 'groupAdminGroup', params: { groupId: group._id } }"
|
||||||
|
>
|
||||||
|
{{ group.name }}
|
||||||
|
</router-link>
|
||||||
<small class="float-right">{{ group._id }}</small>
|
<small class="float-right">{{ group._id }}</small>
|
||||||
</h6>
|
</h6>
|
||||||
<p class="card-text">
|
<p class="card-text">
|
||||||
@@ -175,177 +188,88 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<formRow
|
||||||
v-if="hero.purchased.plan.dateCreated"
|
v-if="hero.purchased.plan.dateCreated"
|
||||||
class="form-group row"
|
v-model="hero.purchased.plan.dateCreated"
|
||||||
>
|
label="Creation date"
|
||||||
<label class="col-sm-3 col-form-label">
|
:suffix="dateFormat(hero.purchased.plan.dateCreated)"
|
||||||
Creation date:
|
/>
|
||||||
</label>
|
<formRow
|
||||||
<div class="col-sm-9">
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
v-model="hero.purchased.plan.dateCreated"
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
>
|
|
||||||
<div class="input-group-append">
|
|
||||||
<strong class="input-group-text">
|
|
||||||
{{ dateFormat(hero.purchased.plan.dateCreated) }}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="hero.purchased.plan.dateCurrentTypeCreated"
|
v-if="hero.purchased.plan.dateCurrentTypeCreated"
|
||||||
class="form-group row"
|
v-model="hero.purchased.plan.dateCurrentTypeCreated"
|
||||||
|
label="Current sub start date"
|
||||||
|
:suffix="dateFormat(hero.purchased.plan.dateCurrentTypeCreated)"
|
||||||
|
/>
|
||||||
|
<formRow
|
||||||
|
v-model="hero.purchased.plan.dateTerminated"
|
||||||
|
label="Termination date"
|
||||||
|
:suffix="dateFormat(hero.purchased.plan.dateTerminated)"
|
||||||
>
|
>
|
||||||
<label class="col-sm-3 col-form-label">
|
<template #suffix>
|
||||||
Current sub start date:
|
<strong class="input-group-text">
|
||||||
</label>
|
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||||
<div class="col-sm-9">
|
</strong>
|
||||||
<div class="input-group">
|
<a
|
||||||
<input
|
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId && !isGroupPlanMember"
|
||||||
v-model="hero.purchased.plan.dateCurrentTypeCreated"
|
v-b-modal.sub_termination_modal
|
||||||
class="form-control"
|
class="btn btn-danger"
|
||||||
type="text"
|
href="#"
|
||||||
>
|
|
||||||
<div class="input-group-append">
|
|
||||||
<strong class="input-group-text">
|
|
||||||
{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group row">
|
|
||||||
<label class="col-sm-3 col-form-label">
|
|
||||||
Termination date:
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
v-model="hero.purchased.plan.dateTerminated"
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
>
|
|
||||||
<div class="input-group-append">
|
|
||||||
<strong class="input-group-text">
|
|
||||||
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
|
||||||
</strong>
|
|
||||||
<a
|
|
||||||
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId"
|
|
||||||
v-b-modal.sub_termination_modal
|
|
||||||
class="btn btn-danger"
|
|
||||||
href="#"
|
|
||||||
>
|
|
||||||
Terminate
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small
|
|
||||||
v-if="!hero.purchased.plan.dateTerminated
|
|
||||||
&& hero.purchased.plan.planId"
|
|
||||||
class="text-success"
|
|
||||||
>
|
>
|
||||||
|
Terminate
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="isSubscribed() && !isCancelled()"
|
||||||
|
#helpText
|
||||||
|
>
|
||||||
|
<span class="text-success">
|
||||||
The subscription does not have a termination date and is active.
|
The subscription does not have a termination date and is active.
|
||||||
</small>
|
</span>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</formRow>
|
||||||
<div class="form-group row">
|
<formRow
|
||||||
<label class="col-sm-3 col-form-label">
|
v-model="hero.purchased.plan.cumulativeCount"
|
||||||
Cumulative months:
|
label="Cumulative months"
|
||||||
</label>
|
input-type="number"
|
||||||
<div class="col-sm-9">
|
help-text="Cumulative subscribed months across subscription periods."
|
||||||
<input
|
/>
|
||||||
v-model="hero.purchased.plan.cumulativeCount"
|
<formRow
|
||||||
class="form-control"
|
v-model="hero.purchased.plan.extraMonths"
|
||||||
type="number"
|
label="Extra months"
|
||||||
min="0"
|
input-type="number"
|
||||||
step="1"
|
help-text="Additional credit that is applied if a subscription is cancelled."
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0"
|
||||||
|
#suffix
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="btn btn-warning"
|
||||||
|
@click="applyExtraMonths"
|
||||||
>
|
>
|
||||||
<small class="text-secondary">
|
Apply Credit
|
||||||
Cumulative subscribed months across subscription periods.
|
</a>
|
||||||
</small>
|
</template>
|
||||||
</div>
|
</formRow>
|
||||||
</div>
|
<formRow
|
||||||
<div class="form-group row">
|
v-model="hero.purchased.plan.hourglassPromoReceived"
|
||||||
<label class="col-sm-3 col-form-label">
|
label="Received hourglass bonus"
|
||||||
Extra months:
|
:suffix="dateFormat(hero.purchased.plan.hourglassPromoReceived)"
|
||||||
</label>
|
/>
|
||||||
<div class="col-sm-9">
|
<formRow
|
||||||
<div class="input-group">
|
v-model="hero.purchased.plan.consecutive.trinkets"
|
||||||
<input
|
label="Mystic Hourglasses"
|
||||||
v-model="hero.purchased.plan.extraMonths"
|
input-type="number"
|
||||||
class="form-control"
|
min="0"
|
||||||
type="number"
|
/>
|
||||||
min="0"
|
<formRow
|
||||||
step="any"
|
v-model="hero.purchased.plan.consecutive.gemCapExtra"
|
||||||
>
|
label="Gem cap increase"
|
||||||
<div class="input-group-append">
|
input-type="number"
|
||||||
<a
|
min="0"
|
||||||
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0"
|
max="26"
|
||||||
class="btn btn-warning"
|
step="2"
|
||||||
@click="applyExtraMonths"
|
/>
|
||||||
>
|
|
||||||
Apply Credit
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small class="text-secondary">
|
|
||||||
Additional credit that is applied if a subscription is cancelled.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group row">
|
|
||||||
<label class="col-sm-3 col-form-label">
|
|
||||||
Received hourglass bonus:
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
v-model="hero.purchased.plan.hourglassPromoReceived"
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
>
|
|
||||||
<div class="input-group-append">
|
|
||||||
<strong class="input-group-text">
|
|
||||||
{{ dateFormat(hero.purchased.plan.hourglassPromoReceived) }}
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group row">
|
|
||||||
<label class="col-sm-3 col-form-label">
|
|
||||||
Mystic Hourglasses:
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<input
|
|
||||||
v-model="hero.purchased.plan.consecutive.trinkets"
|
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group row">
|
|
||||||
<label class="col-sm-3 col-form-label">
|
|
||||||
Gem cap increase:
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<input
|
|
||||||
v-model="hero.purchased.plan.consecutive.gemCapExtra"
|
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="26"
|
|
||||||
step="2"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-3 col-form-label">
|
<label class="col-sm-3 col-form-label">
|
||||||
Total Gem cap:
|
Total Gem cap:
|
||||||
@@ -354,21 +278,13 @@
|
|||||||
{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 24 }}
|
{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 24 }}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<formRow
|
||||||
<label class="col-sm-3 col-form-label">
|
v-model="hero.purchased.plan.gemsBought"
|
||||||
Gems bought this month:
|
label="Gems bought this month"
|
||||||
</label>
|
input-type="number"
|
||||||
<div class="col-sm-9">
|
min="0"
|
||||||
<input
|
:max="hero.purchased.plan.consecutive.gemCapExtra + 24"
|
||||||
v-model="hero.purchased.plan.gemsBought"
|
/>
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
:max="hero.purchased.plan.consecutive.gemCapExtra + 24"
|
|
||||||
step="1"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-3 col-form-label">
|
<label class="col-sm-3 col-form-label">
|
||||||
Mystery Items:
|
Mystery Items:
|
||||||
@@ -391,7 +307,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'"
|
v-if="!isConvertingToGroupPlan && !isGroupPlanMember"
|
||||||
class="form-group row"
|
class="form-group row"
|
||||||
>
|
>
|
||||||
<div class="offset-sm-3 col-sm-9">
|
<div class="offset-sm-3 col-sm-9">
|
||||||
@@ -419,6 +335,79 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<h2>Payment Details</h2>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="offset-sm-3 col-sm-9 mb-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
@click="getSubscriptionPaymentDetails"
|
||||||
|
>
|
||||||
|
Get Subscription Payment Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="paymentDetails"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(value, key) in paymentDetails"
|
||||||
|
:key="key"
|
||||||
|
class="form-group row"
|
||||||
|
>
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
{{ getHumandReadablePaymentDetails(key).label }}:
|
||||||
|
<span
|
||||||
|
:id="`${key}_tooltip`"
|
||||||
|
v-b-tooltip.hover.right="getHumandReadablePaymentDetails(key).help"
|
||||||
|
class="info-icon"
|
||||||
|
>?</span>
|
||||||
|
</label>
|
||||||
|
<strong class="col-sm-9 col-form-label">
|
||||||
|
<span v-if="value === true">Yes</span>
|
||||||
|
<span v-else-if="value === false">No</span>
|
||||||
|
<span
|
||||||
|
v-else-if="value instanceof String && isDate(value)"
|
||||||
|
v-b-tooltip.hover="value"
|
||||||
|
>
|
||||||
|
{{ formatDate(value) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="value === null">---</span>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="offset-sm-3 col-sm-9">
|
||||||
|
<a
|
||||||
|
v-if="hero.purchased.plan.paymentMethod === 'Google'"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
target="_blank"
|
||||||
|
:href="playOrdersUrl"
|
||||||
|
>
|
||||||
|
Play Console
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-else-if="hero.purchased.plan.paymentMethod === 'Paypal'"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
target="_blank"
|
||||||
|
:href="'https://www.paypal.com/billing/subscriptions/' + paymentDetails.customerId"
|
||||||
|
>
|
||||||
|
PayPal Dashboard
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-else-if="hero.purchased.plan.paymentMethod === 'Stripe'"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
target="_blank"
|
||||||
|
:href="'https://dashboard.stripe.com/customers/' + paymentDetails.customerId"
|
||||||
|
>
|
||||||
|
Stripe Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="expand"
|
v-if="expand"
|
||||||
@@ -474,17 +463,36 @@
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.input-group-append {
|
.form-group {
|
||||||
width: auto;
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
.input-group-text {
|
|
||||||
border-bottom-right-radius: 2px;
|
.input-group-append {
|
||||||
border-top-right-radius: 2px;
|
width: auto;
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.8rem;
|
.input-group-text {
|
||||||
color: $gray-200;
|
border-bottom-right-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: $purple-400;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
background-color: $gray-500;
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon:hover {
|
||||||
|
background-color: $purple-400;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -494,10 +502,61 @@ import { getPlanContext } from '@/../../common/script/cron';
|
|||||||
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
||||||
import saveHero from '../mixins/saveHero';
|
import saveHero from '../mixins/saveHero';
|
||||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||||
|
import FormRow from '../../formRow.vue';
|
||||||
|
|
||||||
|
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
|
||||||
|
|
||||||
|
const humandReadablePaymentDetails = {
|
||||||
|
customerId: {
|
||||||
|
label: 'Customer ID',
|
||||||
|
help: 'The unique identifier for the customer in the payment system.',
|
||||||
|
},
|
||||||
|
purchaseDate: {
|
||||||
|
label: 'Purchase Date',
|
||||||
|
help: 'The date when the subscription was purchased or renewed.',
|
||||||
|
},
|
||||||
|
originalPurchaseDate: {
|
||||||
|
label: 'Original Purchase Date',
|
||||||
|
help: 'The date when the subscription was first purchased.',
|
||||||
|
},
|
||||||
|
productId: {
|
||||||
|
label: 'Product ID',
|
||||||
|
help: 'The identifier for the product associated with the subscription.',
|
||||||
|
},
|
||||||
|
transactionId: {
|
||||||
|
label: 'Transaction ID',
|
||||||
|
help: 'The unique identifier for the last transaction in the payment system.',
|
||||||
|
},
|
||||||
|
isCanceled: {
|
||||||
|
label: 'Is Canceled',
|
||||||
|
help: 'Indicates whether the subscription has been canceled by the user or the system.',
|
||||||
|
},
|
||||||
|
isExpired: {
|
||||||
|
label: 'Is Expired',
|
||||||
|
help: 'Indicates whether the subscription has expired. A cancelled subscription may still be active until the end of the billing cycle.',
|
||||||
|
},
|
||||||
|
expirationDate: {
|
||||||
|
label: 'Termination Date',
|
||||||
|
help: 'The date when the subscription will expire or has expired.',
|
||||||
|
},
|
||||||
|
nextPaymentDate: {
|
||||||
|
label: 'Next Payment Date',
|
||||||
|
help: 'The date when the next payment is due. If the subscription is canceled or expired, this may be null.',
|
||||||
|
},
|
||||||
|
lastPaymentDate: {
|
||||||
|
label: 'Last Payment Date',
|
||||||
|
help: 'The date when the lastpayment was made for the subscription.',
|
||||||
|
},
|
||||||
|
failedPayments: {
|
||||||
|
label: 'Failed Payments',
|
||||||
|
help: 'Number of times the payment failed for this subscription.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
|
FormRow,
|
||||||
},
|
},
|
||||||
mixins: [saveHero],
|
mixins: [saveHero],
|
||||||
props: {
|
props: {
|
||||||
@@ -520,6 +579,7 @@ export default {
|
|||||||
isConvertingToGroupPlan: false,
|
isConvertingToGroupPlan: false,
|
||||||
groupPlanID: '',
|
groupPlanID: '',
|
||||||
subscriptionBlocks,
|
subscriptionBlocks,
|
||||||
|
paymentDetails: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -553,6 +613,12 @@ export default {
|
|||||||
}
|
}
|
||||||
return terminationDate;
|
return terminationDate;
|
||||||
},
|
},
|
||||||
|
playOrdersUrl () {
|
||||||
|
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
|
||||||
|
},
|
||||||
|
isGroupPlanMember () {
|
||||||
|
return this.hero.purchased.plan.planId === 'group_plan_auto';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
dateFormat (date) {
|
dateFormat (date) {
|
||||||
@@ -583,6 +649,20 @@ export default {
|
|||||||
this.isConvertingToGroupPlan = true;
|
this.isConvertingToGroupPlan = true;
|
||||||
this.hero.purchased.plan.owner = '';
|
this.hero.purchased.plan.owner = '';
|
||||||
},
|
},
|
||||||
|
getSubscriptionPaymentDetails () {
|
||||||
|
this.$store.dispatch('admin:getSubscriptionPaymentDetails', { userIdentifier: this.hero._id })
|
||||||
|
.then(details => {
|
||||||
|
if (details) {
|
||||||
|
this.paymentDetails = details;
|
||||||
|
} else {
|
||||||
|
alert('No payment details found.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching subscription payment details:', error);
|
||||||
|
alert(`Failed to fetch payment details: ${error.message || 'Unknown error'}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
saveClicked (e) {
|
saveClicked (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.isConvertingToGroupPlan) {
|
if (this.isConvertingToGroupPlan) {
|
||||||
@@ -601,6 +681,30 @@ export default {
|
|||||||
this.$emit('changeUserIdentifier', id);
|
this.$emit('changeUserIdentifier', id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getHumandReadablePaymentDetails (key) {
|
||||||
|
return humandReadablePaymentDetails[key] || { label: key, help: '' };
|
||||||
|
},
|
||||||
|
isDate (date) {
|
||||||
|
return moment(date).isValid();
|
||||||
|
},
|
||||||
|
formatDate (date) {
|
||||||
|
return date ? moment(date).format('MM/DD/YYYY') : '---';
|
||||||
|
},
|
||||||
|
isSubscribed () {
|
||||||
|
return this.hero.purchased.plan
|
||||||
|
&& this.hero.purchased.plan.customerId
|
||||||
|
&& this.hero.purchased.plan.planId
|
||||||
|
&& this.hero.purchased.plan.paymentMethod
|
||||||
|
&& (
|
||||||
|
!this.hero.purchased.plan.dateTerminated
|
||||||
|
|| moment(this.hero.purchased.plan.dateTerminated).isAfter(moment())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isCancelled () {
|
||||||
|
return this.hero.purchased.plan
|
||||||
|
&& this.hero.purchased.plan.dateTerminated
|
||||||
|
&& this.hero.purchased.plan.dateTerminated !== '';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async retrieveUserHistory () {
|
async retrieveUserHistory () {
|
||||||
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
|
const history = await this.$store.dispatch('admin:getUserHistory', { userIdentifier: this.hero._id });
|
||||||
this.armoire = history.armoire;
|
this.armoire = history.armoire;
|
||||||
this.questInviteResponses = history.questInviteResponses;
|
this.questInviteResponses = history.questInviteResponses;
|
||||||
this.cron = history.cron;
|
this.cron = history.cron;
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
>
|
>
|
||||||
{{ $t('adminPanel') }}
|
{{ $t('adminPanel') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
v-if="hasPermission(user, 'groupSupport')"
|
||||||
|
class="nav-link"
|
||||||
|
:to="{name: 'groupAdmin'}"
|
||||||
|
>
|
||||||
|
{{ $t('groupAdmin') }}
|
||||||
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="hasPermission(user, 'accessControl')"
|
v-if="hasPermission(user, 'accessControl')"
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
|
|||||||
129
website/client/src/components/admin/formRow.vue
Normal file
129
website/client/src/components/admin/formRow.vue
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label"><slot name="label">{{ label }}:</slot></label>
|
||||||
|
<div
|
||||||
|
class="col-sm-9"
|
||||||
|
:class="editable ? 'editable' : 'col-form-label'"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<div class="input-group">
|
||||||
|
<strong v-if="!editable">
|
||||||
|
{{ value || "---" }}
|
||||||
|
</strong>
|
||||||
|
<textarea
|
||||||
|
v-else-if="inputType === 'textarea'"
|
||||||
|
:value="value"
|
||||||
|
class="form-control"
|
||||||
|
:rows="rows"
|
||||||
|
@input="$emit('input', $event.target.value)"
|
||||||
|
></textarea>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
:value="value"
|
||||||
|
class="form-control"
|
||||||
|
:type="inputType"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="step"
|
||||||
|
@input="$emit('input', $event.target.value)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="suffix || $slots.suffix"
|
||||||
|
class="input-group-append"
|
||||||
|
>
|
||||||
|
<slot name="suffix">
|
||||||
|
<strong class="input-group-text">
|
||||||
|
{{ suffix }}
|
||||||
|
</strong>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
<div
|
||||||
|
v-if="helpText || $slots.helpText"
|
||||||
|
class="form-text text-muted"
|
||||||
|
>
|
||||||
|
<slot name="helpText">
|
||||||
|
{{ helpText }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="$slots.subtitle"
|
||||||
|
class="form-text text-muted mt-1"
|
||||||
|
>
|
||||||
|
<slot name="subtitle"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/assets/scss/colors.scss';
|
||||||
|
.input-group-append {
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { max, min } from 'lodash';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'value',
|
||||||
|
event: 'input',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: [String, Boolean, Number],
|
||||||
|
},
|
||||||
|
inputType: {
|
||||||
|
type: String,
|
||||||
|
default: 'text',
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
helpText: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
suffix: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 0,
|
||||||
|
validator (value) {
|
||||||
|
return !isNaN(value) && min([value, 0]) === 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: [Number, String],
|
||||||
|
validator (value) {
|
||||||
|
return !isNaN(value) && max([value, 100]) === 100;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 1,
|
||||||
|
validator (value) {
|
||||||
|
return !isNaN(value) && min([value, 1]) === 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<form>
|
||||||
|
<form-row
|
||||||
|
v-model="group.type"
|
||||||
|
label="Group Type"
|
||||||
|
:editable="false"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.name"
|
||||||
|
:label="$t('groupName')"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.summary"
|
||||||
|
:label="$t('guildSummary')"
|
||||||
|
input-type="textarea"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.chatLimitCount"
|
||||||
|
label="Chat limit"
|
||||||
|
input-type="number"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.description"
|
||||||
|
:label="$t('groupDescription')"
|
||||||
|
input-type="textarea"
|
||||||
|
rows="6"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.bannedWordsAllowed"
|
||||||
|
:label="$t('bannedWordsAllowed')"
|
||||||
|
input-type="checkbox"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.leaderOnly.challenges"
|
||||||
|
:label="$t('leaderOnlyChallenges')"
|
||||||
|
input-type="checkbox"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import formRow from '@/components/admin/formRow.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
formRow,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
group: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="group.purchased.plan">
|
||||||
|
<form-row
|
||||||
|
v-model="group.purchased.plan.paymentMethod"
|
||||||
|
label="Payment Method"
|
||||||
|
:editable="false"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.purchased.plan.planId"
|
||||||
|
label="Plan ID"
|
||||||
|
:editable="false"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.purchased.plan.customerId"
|
||||||
|
label="Customer ID"
|
||||||
|
:editable="false"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.purchased.plan.dateCreated"
|
||||||
|
label="Creation Date"
|
||||||
|
:editable="false"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.purchased.plan.dateTerminated"
|
||||||
|
label="Termination Date"
|
||||||
|
:editable="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import formRow from '@/components/admin/formRow.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
formRow,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
group: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="hasPermission(user, 'groupSupport')">
|
||||||
|
<h2>{{ group.name }}</h2>
|
||||||
|
<router-link
|
||||||
|
v-if="isGroupPlan"
|
||||||
|
:to="{'name': 'groupPlanDetail', 'params': {'groupId': groupId}}"
|
||||||
|
>
|
||||||
|
Group Plan Page
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<supportContainer
|
||||||
|
:title="$t('groupData')"
|
||||||
|
:on-save="updateGroup"
|
||||||
|
>
|
||||||
|
<groupData
|
||||||
|
:group="group"
|
||||||
|
/>
|
||||||
|
</supportContainer>
|
||||||
|
<supportContainer
|
||||||
|
:title="$t('groupPlanSubscription')"
|
||||||
|
>
|
||||||
|
<groupPlan
|
||||||
|
:group="group"
|
||||||
|
/>
|
||||||
|
</supportContainer>
|
||||||
|
<supportContainer
|
||||||
|
v-if="group.type === 'party'"
|
||||||
|
:title="$t('questDetails')"
|
||||||
|
>
|
||||||
|
<quest
|
||||||
|
:group="group"
|
||||||
|
/>
|
||||||
|
</supportContainer>
|
||||||
|
<supportContainer
|
||||||
|
:title="$t('members')"
|
||||||
|
>
|
||||||
|
<members
|
||||||
|
:group="group"
|
||||||
|
/>
|
||||||
|
</supportContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { userStateMixin } from '../../../../mixins/userState';
|
||||||
|
import supportContainer from '../../supportContainer.vue';
|
||||||
|
import groupData from './groupData.vue';
|
||||||
|
import members from './members.vue';
|
||||||
|
import groupPlan from './groupPlan.vue';
|
||||||
|
import quest from './quest.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
supportContainer,
|
||||||
|
groupData,
|
||||||
|
members,
|
||||||
|
groupPlan,
|
||||||
|
quest,
|
||||||
|
},
|
||||||
|
mixins: [userStateMixin],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
groupId: '',
|
||||||
|
group: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isGroupPlan () {
|
||||||
|
return this.group
|
||||||
|
&& this.group.purchased
|
||||||
|
&& this.group.purchased.plan
|
||||||
|
&& this.group.purchased.plan.planId;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
groupId () {
|
||||||
|
this.loadGroup(this.groupId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.groupId = this.$route.params.groupId;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearData () {
|
||||||
|
this.group = {};
|
||||||
|
},
|
||||||
|
async loadGroup (groupId) {
|
||||||
|
this.$emit('changeGroupId', groupId);
|
||||||
|
this.group = await this.$store.dispatch('admin:getGroup', { groupId });
|
||||||
|
},
|
||||||
|
async updateGroup () {
|
||||||
|
if (this.group && !this.group.id) {
|
||||||
|
this.group.id = this.group._id || this.groupId; // Ensure group has an id property
|
||||||
|
}
|
||||||
|
await this.$store.dispatch('guilds:update', { group: this.group });
|
||||||
|
this.group = await this.$store.dispatch('admin:getGroup', { groupId: this.group.id });
|
||||||
|
await this.$store.dispatch('snackbars:add', {
|
||||||
|
title: '',
|
||||||
|
text: 'Group updated',
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<form-row
|
||||||
|
:label="$t('groupLeader')"
|
||||||
|
>
|
||||||
|
<strong class="col-form-label">
|
||||||
|
<router-link
|
||||||
|
:to="{'name': 'adminPanelUser', 'params': {'userIdentifier': group.leader }}"
|
||||||
|
>
|
||||||
|
{{ group.leader }}
|
||||||
|
</router-link>
|
||||||
|
</strong>
|
||||||
|
</form-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import formRow from '@/components/admin/formRow.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
formRow,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
group: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<form-row
|
||||||
|
v-model="group.quest.key"
|
||||||
|
label="Quest Identifier"
|
||||||
|
:editable="false"
|
||||||
|
/>
|
||||||
|
<form-row
|
||||||
|
v-model="group.quest.leader"
|
||||||
|
label="Quest Leader"
|
||||||
|
:editable="false"
|
||||||
|
>
|
||||||
|
<template slot="subtitle">
|
||||||
|
<router-link
|
||||||
|
:to="{'name': 'adminPanelUser', 'params': {'userIdentifier': group.quest.leader}}"
|
||||||
|
>
|
||||||
|
{{ group.quest.leader }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</form-row>
|
||||||
|
<form-row
|
||||||
|
v-model="group.quest.active"
|
||||||
|
label="Is Quest Active"
|
||||||
|
input-type="checkbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import formRow from '@/components/admin/formRow.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
formRow,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
group: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
93
website/client/src/components/admin/groups/index.vue
Normal file
93
website/client/src/components/admin/groups/index.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row standard-page col-12 d-flex justify-content-center">
|
||||||
|
<div class="group-admin-content">
|
||||||
|
<h1>{{ $t("groupAdmin") }}</h1>
|
||||||
|
<form
|
||||||
|
class="form-inline"
|
||||||
|
@submit.prevent="loadGroup(groupID)"
|
||||||
|
>
|
||||||
|
<div class="input-group col pl-0 pr-0">
|
||||||
|
<input
|
||||||
|
v-model="groupID"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
placeholder="Group ID"
|
||||||
|
>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
:disabled="!groupID"
|
||||||
|
@click="loadGroup(groupID)"
|
||||||
|
>
|
||||||
|
Load
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<router-view
|
||||||
|
class="mt-3"
|
||||||
|
@changeGroupId="changeGroupId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.uidField {
|
||||||
|
min-width: 45ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-append {
|
||||||
|
width:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-admin-content {
|
||||||
|
flex: 0 0 800px;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VueRouter from 'vue-router';
|
||||||
|
import { mapState } from '@/libs/store';
|
||||||
|
|
||||||
|
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
groupID: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({ user: 'user.data' }),
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.$store.dispatch('common:setTitle', {
|
||||||
|
section: this.$t('groupAdmin'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeGroupId (id) {
|
||||||
|
this.groupID = id;
|
||||||
|
},
|
||||||
|
async loadGroup (groupId) {
|
||||||
|
if (this.$router.currentRoute.name === 'groupAdminGroup') {
|
||||||
|
await this.$router.push({
|
||||||
|
name: 'groupAdmin',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await this.$router.push({
|
||||||
|
name: 'groupAdminGroup',
|
||||||
|
params: { groupId },
|
||||||
|
}).catch(failure => {
|
||||||
|
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||||
|
this.$router.go();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
53
website/client/src/components/admin/supportContainer.vue
Normal file
53
website/client/src/components/admin/supportContainer.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3
|
||||||
|
class="mb-0 mt-0"
|
||||||
|
:class="{'open': expand}"
|
||||||
|
@click="expand = !expand"
|
||||||
|
>
|
||||||
|
<slot name="title">
|
||||||
|
{{ title }}
|
||||||
|
</slot>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand && onSave"
|
||||||
|
class="card-footer"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary mt-1"
|
||||||
|
@click="onSave"
|
||||||
|
>
|
||||||
|
{{ $t('save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
onSave: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
expand: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1330,7 +1330,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openAdminPanel () {
|
openAdminPanel () {
|
||||||
this.$router.push(`/admin-panel/${this.hero._id}`);
|
this.$router.push(`/admin/panel/${this.hero._id}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/
|
|||||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
|
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
|
||||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
|
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
|
||||||
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
|
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
|
||||||
|
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
|
||||||
|
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
|
||||||
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
|
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
@@ -222,6 +224,28 @@ const router = new VueRouter({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'groupAdmin',
|
||||||
|
path: 'groups',
|
||||||
|
component: GroupAdminPage,
|
||||||
|
meta: {
|
||||||
|
privilegeNeeded: [ // any one of these is enough to give access
|
||||||
|
'groupSupport',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'groupAdminGroup',
|
||||||
|
path: ':groupId',
|
||||||
|
component: GroupAdminGroupPage,
|
||||||
|
meta: {
|
||||||
|
privilegeNeeded: [
|
||||||
|
'groupsSupport',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'blockers',
|
name: 'blockers',
|
||||||
path: 'blockers',
|
path: 'blockers',
|
||||||
|
|||||||
31
website/client/src/store/actions/admin.js
Normal file
31
website/client/src/store/actions/admin.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export async function searchUsers (store, payload) {
|
||||||
|
const url = `/api/v4/admin/search/${payload.userIdentifier}`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserHistory (store, payload) {
|
||||||
|
const url = `/api/v4/admin/user/${payload.userIdentifier}/history`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSubscriptionPaymentDetails (store, payload) {
|
||||||
|
const url = `/api/v4/admin/user/${payload.userIdentifier}/subscription-payment-details`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGroup (store, payload) {
|
||||||
|
const url = `/api/v4/admin/groups/${payload.groupId}`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateGroup (store, payload) {
|
||||||
|
const url = `/api/v4/admin/groups/${payload.groupId || payload.group._id}`;
|
||||||
|
const response = await axios.put(url, payload.group);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
export async function searchUsers (store, payload) {
|
|
||||||
const url = `/api/v4/admin/search/${payload.userIdentifier}`;
|
|
||||||
const response = await axios.get(url);
|
|
||||||
return response.data.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUserHistory (store, payload) {
|
|
||||||
const url = `/api/v4/admin/user/${payload.userIdentifier}/history`;
|
|
||||||
const response = await axios.get(url);
|
|
||||||
return response.data.data;
|
|
||||||
}
|
|
||||||
@@ -38,3 +38,9 @@ export async function getHeroGroupPlans (store, payload) {
|
|||||||
const response = await axios.get(url);
|
const response = await axios.get(url);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteHero (store, payload) {
|
||||||
|
const url = `/api/v4/members/${payload.uuid}?deleteAccount=${payload.deleteHabiticaAccount}&deleteAmplitude=${payload.deleteAmplitudeData}`;
|
||||||
|
const response = await axios.delete(url);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { flattenAndNamespace } from '@/libs/store/helpers/internals';
|
import { flattenAndNamespace } from '@/libs/store/helpers/internals';
|
||||||
|
|
||||||
import * as adminPanel from './adminPanel';
|
import * as admin from './admin';
|
||||||
import * as common from './common';
|
import * as common from './common';
|
||||||
import * as user from './user';
|
import * as user from './user';
|
||||||
import * as tasks from './tasks';
|
import * as tasks from './tasks';
|
||||||
@@ -26,7 +26,7 @@ import * as blockers from './blockers';
|
|||||||
// Example: fetch in user.js -> 'user:fetch'
|
// Example: fetch in user.js -> 'user:fetch'
|
||||||
|
|
||||||
const actions = flattenAndNamespace({
|
const actions = flattenAndNamespace({
|
||||||
adminPanel,
|
admin,
|
||||||
common,
|
common,
|
||||||
user,
|
user,
|
||||||
tasks,
|
tasks,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const envVars = [
|
|||||||
'TIME_TRAVEL_ENABLED',
|
'TIME_TRAVEL_ENABLED',
|
||||||
'DEBUG_ENABLED',
|
'DEBUG_ENABLED',
|
||||||
'CONTENT_SWITCHOVER_TIME_OFFSET',
|
'CONTENT_SWITCHOVER_TIME_OFFSET',
|
||||||
// TODO necessary? if yes how not to mess up with vue cli? 'NODE_ENV'
|
'PLAY_CONSOLE_ORDERS_BASE_URL',
|
||||||
];
|
];
|
||||||
|
|
||||||
const envObject = {};
|
const envObject = {};
|
||||||
|
|||||||
@@ -3,5 +3,9 @@
|
|||||||
"siteBlockers": "Site Blockers",
|
"siteBlockers": "Site Blockers",
|
||||||
"newsroom": "Newsroom",
|
"newsroom": "Newsroom",
|
||||||
"adminBlockerTypeDescription": "<b>IP-Address</b> - Block access for a specific IP-Address\n\nClient - Block access for a client based on the \"x-client\" header.\n\nE-Mail - Blocks e-mails from being used for signup.",
|
"adminBlockerTypeDescription": "<b>IP-Address</b> - Block access for a specific IP-Address\n\nClient - Block access for a client based on the \"x-client\" header.\n\nE-Mail - Blocks e-mails from being used for signup.",
|
||||||
"adminBlockerAreaDescription": "A blocker can either apply to the full site, completely blocking any access. Or it can apply to purchases, which still allows the site to be accessed."
|
"adminBlockerAreaDescription": "A blocker can either apply to the full site, completely blocking any access. Or it can apply to purchases, which still allows the site to be accessed.",
|
||||||
|
"groupAdmin": "Group Admin",
|
||||||
|
"groupSupportDescription": "Manage groups and their members. You can search for groups by ID, or load your own group by leaving the field blank.",
|
||||||
|
"groupData": "Group Data",
|
||||||
|
"groupPlanSubscription": "Group Plan Subscription"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,16 +320,16 @@ api.updateHero = {
|
|||||||
if (plan.extraMonths || plan.extraMonths === 0) {
|
if (plan.extraMonths || plan.extraMonths === 0) {
|
||||||
hero.purchased.plan.extraMonths = plan.extraMonths;
|
hero.purchased.plan.extraMonths = plan.extraMonths;
|
||||||
}
|
}
|
||||||
if (plan.customerId) {
|
if (plan.customerId || plan.customerId === '') {
|
||||||
hero.purchased.plan.customerId = plan.customerId;
|
hero.purchased.plan.customerId = plan.customerId;
|
||||||
}
|
}
|
||||||
if (plan.paymentMethod) {
|
if (plan.paymentMethod || plan.customerId === '') {
|
||||||
hero.purchased.plan.paymentMethod = plan.paymentMethod;
|
hero.purchased.plan.paymentMethod = plan.paymentMethod;
|
||||||
}
|
}
|
||||||
if (plan.planId) {
|
if (plan.planId || plan.customerId === '') {
|
||||||
hero.purchased.plan.planId = plan.planId;
|
hero.purchased.plan.planId = plan.planId;
|
||||||
}
|
}
|
||||||
if (plan.owner) {
|
if (plan.owner || plan.customerId === '') {
|
||||||
hero.purchased.plan.owner = plan.owner;
|
hero.purchased.plan.owner = plan.owner;
|
||||||
}
|
}
|
||||||
if (plan.hourglassPromoReceived) {
|
if (plan.hourglassPromoReceived) {
|
||||||
@@ -341,8 +341,7 @@ api.updateHero = {
|
|||||||
const group = await Group.getGroup({ user: hero, groupId: groupID });
|
const group = await Group.getGroup({ user: hero, groupId: groupID });
|
||||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||||
if (group.hasNotCancelled()) {
|
if (group.hasNotCancelled()) {
|
||||||
hero.purchased.plan.customerId = null;
|
hero.purchased.plan.paymentMethod = 'groupPlan';
|
||||||
hero.purchased.plan.paymentMethod = null;
|
|
||||||
await addSubToGroupUser(hero, group);
|
await addSubToGroupUser(hero, group);
|
||||||
await group.updateGroupPlan();
|
await group.updateGroupPlan();
|
||||||
} else {
|
} else {
|
||||||
@@ -352,34 +351,34 @@ api.updateHero = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateData.stats) {
|
if (updateData.stats) {
|
||||||
if (updateData.stats.hp) {
|
if (updateData.stats.hp || updateData.stats.hp === 0) {
|
||||||
hero.stats.hp = updateData.stats.hp;
|
hero.stats.hp = updateData.stats.hp;
|
||||||
}
|
}
|
||||||
if (updateData.stats.mp) {
|
if (updateData.stats.mp || updateData.stats.mp === 0) {
|
||||||
hero.stats.mp = updateData.stats.mp;
|
hero.stats.mp = updateData.stats.mp;
|
||||||
}
|
}
|
||||||
if (updateData.stats.exp) {
|
if (updateData.stats.exp || updateData.stats.exp === 0) {
|
||||||
hero.stats.exp = updateData.stats.exp;
|
hero.stats.exp = updateData.stats.exp;
|
||||||
}
|
}
|
||||||
if (updateData.stats.gp) {
|
if (updateData.stats.gp || updateData.stats.gp === 0) {
|
||||||
hero.stats.gp = updateData.stats.gp;
|
hero.stats.gp = updateData.stats.gp;
|
||||||
}
|
}
|
||||||
if (updateData.stats.lvl) {
|
if (updateData.stats.lvl || updateData.stats.lvl === 0) {
|
||||||
hero.stats.lvl = updateData.stats.lvl;
|
hero.stats.lvl = updateData.stats.lvl;
|
||||||
}
|
}
|
||||||
if (updateData.stats.points) {
|
if (updateData.stats.points || updateData.stats.points === 0) {
|
||||||
hero.stats.points = updateData.stats.points;
|
hero.stats.points = updateData.stats.points;
|
||||||
}
|
}
|
||||||
if (updateData.stats.str) {
|
if (updateData.stats.str || updateData.stats.str === 0) {
|
||||||
hero.stats.str = updateData.stats.str;
|
hero.stats.str = updateData.stats.str;
|
||||||
}
|
}
|
||||||
if (updateData.stats.int) {
|
if (updateData.stats.int || updateData.stats.int === 0) {
|
||||||
hero.stats.int = updateData.stats.int;
|
hero.stats.int = updateData.stats.int;
|
||||||
}
|
}
|
||||||
if (updateData.stats.per) {
|
if (updateData.stats.per || updateData.stats.per === 0) {
|
||||||
hero.stats.per = updateData.stats.per;
|
hero.stats.per = updateData.stats.per;
|
||||||
}
|
}
|
||||||
if (updateData.stats.con) {
|
if (updateData.stats.con || updateData.stats.con === 0) {
|
||||||
hero.stats.con = updateData.stats.con;
|
hero.stats.con = updateData.stats.con;
|
||||||
}
|
}
|
||||||
if (updateData.stats.buffs) {
|
if (updateData.stats.buffs) {
|
||||||
@@ -511,13 +510,22 @@ api.updateHero = {
|
|||||||
const savedHero = await hero.save();
|
const savedHero = await hero.save();
|
||||||
|
|
||||||
if (updateData.removeFromParty) {
|
if (updateData.removeFromParty) {
|
||||||
await leaveGroup({
|
try {
|
||||||
user: savedHero,
|
await leaveGroup({
|
||||||
groupId: savedHero.party._id,
|
user: savedHero,
|
||||||
res,
|
groupId: savedHero.party._id,
|
||||||
keep: false,
|
res,
|
||||||
keepChallenges: false,
|
keep: false,
|
||||||
});
|
keepChallenges: false,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof NotFound) {
|
||||||
|
savedHero.party = null; // Party does not exist, so just unset it
|
||||||
|
await savedHero.save();
|
||||||
|
} else {
|
||||||
|
throw err; // re-throw other errors
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const heroJSON = savedHero.toJSON();
|
const heroJSON = savedHero.toJSON();
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
|
import uniqBy from 'lodash/uniqBy';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { authWithHeaders } from '../../middlewares/auth';
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
import { model as UserHistory } from '../../models/userHistory';
|
import { model as UserHistory } from '../../models/userHistory';
|
||||||
|
import { model as Group } from '../../models/group';
|
||||||
import { model as Blocker } from '../../models/blocker';
|
import { model as Blocker } from '../../models/blocker';
|
||||||
import {
|
import {
|
||||||
NotFound,
|
NotFound,
|
||||||
} from '../../libs/errors';
|
} from '../../libs/errors';
|
||||||
|
import apple from '../../libs/payments/apple';
|
||||||
|
import google from '../../libs/payments/google';
|
||||||
|
import paypal from '../../libs/payments/paypal';
|
||||||
|
import {
|
||||||
|
getSubscriptionPaymentDetails as getStripeSubscriptionPaymentDetails,
|
||||||
|
} from '../../libs/payments/stripe/subscriptions';
|
||||||
|
|
||||||
const api = {};
|
const api = {};
|
||||||
|
|
||||||
@@ -40,8 +48,6 @@ api.searchHero = {
|
|||||||
|
|
||||||
const { userIdentifier } = req.params;
|
const { userIdentifier } = req.params;
|
||||||
|
|
||||||
const re = new RegExp(String.raw`^${userIdentifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
|
||||||
|
|
||||||
let query;
|
let query;
|
||||||
let users = [];
|
let users = [];
|
||||||
if (validator.isUUID(userIdentifier)) {
|
if (validator.isUUID(userIdentifier)) {
|
||||||
@@ -54,7 +60,7 @@ api.searchHero = {
|
|||||||
'auth.facebook.emails.value',
|
'auth.facebook.emails.value',
|
||||||
];
|
];
|
||||||
for (const field of emailFields) {
|
for (const field of emailFields) {
|
||||||
const emailQuery = { [field]: userIdentifier };
|
const emailQuery = { [field]: userIdentifier.toLowerCase() };
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const found = await User.findOne(emailQuery)
|
const found = await User.findOne(emailQuery)
|
||||||
.select('contributor backer profile auth')
|
.select('contributor backer profile auth')
|
||||||
@@ -65,6 +71,7 @@ api.searchHero = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const re = new RegExp(String.raw`^${userIdentifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||||
query = { 'auth.local.lowerCaseUsername': { $regex: re, $options: 'i' } };
|
query = { 'auth.local.lowerCaseUsername': { $regex: re, $options: 'i' } };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +83,8 @@ api.searchHero = {
|
|||||||
.lean()
|
.lean()
|
||||||
.exec();
|
.exec();
|
||||||
}
|
}
|
||||||
res.respond(200, users);
|
|
||||||
|
res.respond(200, uniqBy(users, '_id'));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -188,4 +196,68 @@ api.deleteBlocker = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
api.validateSubscriptionPaymentDetails = {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/admin/user/:userId/subscription-payment-details',
|
||||||
|
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||||
|
async handler (req, res) {
|
||||||
|
req.checkParams('userId', res.t('heroIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
|
const validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
const { userId } = req.params;
|
||||||
|
|
||||||
|
const user = await User.findById(userId)
|
||||||
|
.select('purchased')
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (!user) throw new NotFound(res.t('userWithIDNotFound', { userId }));
|
||||||
|
if (!user.purchased || !user.purchased.plan || !user.purchased.plan.paymentMethod || !user.purchased.plan.paymentMethod === '') {
|
||||||
|
throw new NotFound(res.t('subscriptionNotFoundForUser', { userId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let paymentDetails;
|
||||||
|
if (user.purchased.plan.paymentMethod === 'Apple') {
|
||||||
|
paymentDetails = await apple.getSubscriptionPaymentDetails(userId, user.purchased.plan);
|
||||||
|
} else if (user.purchased.plan.paymentMethod === 'Google') {
|
||||||
|
paymentDetails = await google.getSubscriptionPaymentDetails(userId, user.purchased.plan);
|
||||||
|
} else if (user.purchased.plan.paymentMethod === 'Paypal') {
|
||||||
|
paymentDetails = await paypal.getSubscriptionPaymentDetails({ user });
|
||||||
|
} else if (user.purchased.plan.paymentMethod === 'Stripe') {
|
||||||
|
paymentDetails = await getStripeSubscriptionPaymentDetails(user);
|
||||||
|
} else if (user.purchased.plan.paymentMethod === 'Amazon Payments') {
|
||||||
|
throw new NotFound(res.t('amazonSubscriptionNotValidated'));
|
||||||
|
} else if (user.purchased.plan.paymentMethod === 'Gift') {
|
||||||
|
throw new NotFound(res.t('giftSubscriptionNotValidated'));
|
||||||
|
} else {
|
||||||
|
throw new NotFound(res.t('unknownSubscriptionPaymentMethod', { method: user.purchased.paymentMethod }));
|
||||||
|
}
|
||||||
|
res.respond(200, paymentDetails);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
api.getGroup = {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/admin/groups/:groupId',
|
||||||
|
middlewares: [authWithHeaders(), ensurePermission('groupSupport')],
|
||||||
|
async handler (req, res) {
|
||||||
|
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
|
const validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
const { groupId } = req.params;
|
||||||
|
|
||||||
|
const group = await Group.findById(groupId)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||||
|
|
||||||
|
res.respond(200, group);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { sendJob } from '../../libs/worker';
|
||||||
import { authWithHeaders } from '../../middlewares/auth';
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||||
import { TransactionModel as Transaction } from '../../models/transaction';
|
import { TransactionModel as Transaction } from '../../models/transaction';
|
||||||
@@ -5,9 +6,9 @@ import { TransactionModel as Transaction } from '../../models/transaction';
|
|||||||
const api = {};
|
const api = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /api/v4/user/purchase-history Get users purchase history
|
* @api {get} /api/v4/members/:memberId/purchase-history Get members purchase history
|
||||||
* @apiName UserGetPurchaseHistory
|
* @apiName MemberGetPurchaseHistory
|
||||||
* @apiGroup User
|
* @apiGroup Member
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
api.purchaseHistory = {
|
api.purchaseHistory = {
|
||||||
@@ -31,4 +32,31 @@ api.purchaseHistory = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {delete} /api/v4/members/:memberId Delete a user
|
||||||
|
* @apiName DeleteMember
|
||||||
|
* @apiGroup Member
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
api.deleteMember = {
|
||||||
|
method: 'DELETE',
|
||||||
|
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||||
|
url: '/members/:memberId',
|
||||||
|
async handler (req, res) {
|
||||||
|
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||||
|
req.checkQuery('deleteAccount').optional().isIn(['true', 'false']);
|
||||||
|
req.checkQuery('deleteAmplitude').optional().isIn(['true', 'false']);
|
||||||
|
const validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
sendJob('delete-user', {
|
||||||
|
data: {
|
||||||
|
userId: req.params.memberId,
|
||||||
|
deleteAccount: req.query.deleteAccount === 'true',
|
||||||
|
deleteAmplitude: req.query.deleteAmplitude === 'true',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.respond(200, {});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import got from 'got';
|
|
||||||
import { TAVERN_ID } from '../models/group'; // eslint-disable-line import/no-cycle
|
import { TAVERN_ID } from '../models/group'; // eslint-disable-line import/no-cycle
|
||||||
import { encrypt } from './encryption';
|
import { encrypt } from './encryption';
|
||||||
import logger from './logger';
|
|
||||||
import common from '../../common';
|
import common from '../../common';
|
||||||
|
import { sendJob } from './worker';
|
||||||
|
|
||||||
const IS_PROD = nconf.get('IS_PROD');
|
const IS_PROD = nconf.get('IS_PROD');
|
||||||
const EMAIL_SERVER = {
|
|
||||||
url: nconf.get('EMAIL_SERVER_URL'),
|
|
||||||
auth: {
|
|
||||||
user: nconf.get('EMAIL_SERVER_AUTH_USER'),
|
|
||||||
password: nconf.get('EMAIL_SERVER_AUTH_PASSWORD'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const BASE_URL = nconf.get('BASE_URL');
|
const BASE_URL = nconf.get('BASE_URL');
|
||||||
|
|
||||||
export function getUserInfo (user, fields = []) {
|
export function getUserInfo (user, fields = []) {
|
||||||
@@ -156,29 +148,14 @@ export async function sendTxn (mailingInfoArray, emailType, variables, personalV
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (IS_PROD && mailingInfoArray.length > 0) {
|
if (IS_PROD && mailingInfoArray.length > 0) {
|
||||||
return got.post(`${EMAIL_SERVER.url}/job`, {
|
return sendJob('email', {
|
||||||
retry: 5, // retry the http request to the email server 5 times
|
data: {
|
||||||
timeout: 60000, // wait up to 60s before timing out
|
emailType,
|
||||||
username: EMAIL_SERVER.auth.user,
|
to: mailingInfoArray,
|
||||||
password: EMAIL_SERVER.auth.password,
|
variables,
|
||||||
json: {
|
personalVariables,
|
||||||
type: 'email',
|
|
||||||
data: {
|
|
||||||
emailType,
|
|
||||||
to: mailingInfoArray,
|
|
||||||
variables,
|
|
||||||
personalVariables,
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
priority: 'high',
|
|
||||||
attempts: 5,
|
|
||||||
backoff: { delay: 10 * 60 * 1000, type: 'fixed' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}).json().catch(err => logger.error(err, {
|
});
|
||||||
extraMessage: 'Error while sending an email.',
|
|
||||||
emailType,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
|||||||
return appleRes;
|
return appleRes;
|
||||||
};
|
};
|
||||||
|
|
||||||
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
|
async function findSubscriptionPurchase (receipt, onlyActive = true) {
|
||||||
await iap.setup();
|
await iap.setup();
|
||||||
|
|
||||||
const appleRes = await iap.validate(iap.APPLE, receipt);
|
const appleRes = await iap.validate(iap.APPLE, receipt);
|
||||||
@@ -85,18 +85,56 @@ api.subscribe = async function subscribe (user, receipt, headers, nextPaymentPro
|
|||||||
if (purchaseDataList.length === 0) {
|
if (purchaseDataList.length === 0) {
|
||||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||||
}
|
}
|
||||||
|
|
||||||
let purchase;
|
let purchase;
|
||||||
let newestDate;
|
let newestDate;
|
||||||
|
|
||||||
for (const purchaseData of purchaseDataList) {
|
for (const purchaseData of purchaseDataList) {
|
||||||
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
let datePurchased;
|
||||||
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
if (purchaseData.purchaseDate instanceof Date) {
|
||||||
if ((!newestDate || datePurchased > newestDate) && dateTerminated > new Date()) {
|
datePurchased = purchaseData.purchaseDate;
|
||||||
purchase = purchaseData;
|
} else {
|
||||||
newestDate = datePurchased;
|
datePurchased = new Date(Number(purchaseData.purchaseDateMs || purchaseData.purchaseDate));
|
||||||
|
}
|
||||||
|
const dateTerminated = new Date(Number(purchaseData.expirationDate || 0));
|
||||||
|
if ((!newestDate || datePurchased > newestDate)) {
|
||||||
|
if (!onlyActive || dateTerminated > new Date()) {
|
||||||
|
purchase = purchaseData;
|
||||||
|
newestDate = datePurchased;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!purchase) {
|
||||||
|
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
purchase,
|
||||||
|
isCanceled: iap.isCanceled(purchase),
|
||||||
|
isExpired: iap.isExpired(purchase),
|
||||||
|
expirationDate: new Date(Number(purchase.expirationDate)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
api.getSubscriptionPaymentDetails = async function getDetails (userId, subscriptionPlan) {
|
||||||
|
if (!subscriptionPlan || !subscriptionPlan.additionalData) {
|
||||||
|
throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
||||||
|
}
|
||||||
|
const details = await findSubscriptionPurchase(subscriptionPlan.additionalData);
|
||||||
|
return {
|
||||||
|
customerId: details.purchase.originalTransactionId || details.purchase.transactionId,
|
||||||
|
purchaseDate: new Date(Number(details.purchase.purchaseDateMs)),
|
||||||
|
originalPurchaseDate: new Date(Number(details.purchase.originalPurchaseDateMs)),
|
||||||
|
expirationDate: details.isCanceled || details.isExpired ? details.expirationDate : null,
|
||||||
|
nextPaymentDate: details.isCanceled || details.isExpired ? null : details.expirationDate,
|
||||||
|
productId: details.purchase.productId,
|
||||||
|
transactionId: details.purchase.transactionId,
|
||||||
|
isCanceled: details.isCanceled,
|
||||||
|
isExpired: details.isExpired,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
|
||||||
|
const details = await findSubscriptionPurchase(receipt);
|
||||||
|
const { purchase } = details;
|
||||||
|
|
||||||
let subCode;
|
let subCode;
|
||||||
switch (purchase.productId) { // eslint-disable-line default-case
|
switch (purchase.productId) { // eslint-disable-line default-case
|
||||||
@@ -250,37 +288,17 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
|
|||||||
|
|
||||||
api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||||
const { plan } = user.purchased;
|
const { plan } = user.purchased;
|
||||||
|
|
||||||
if (plan.paymentMethod !== api.constants.PAYMENT_METHOD_APPLE) throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
if (plan.paymentMethod !== api.constants.PAYMENT_METHOD_APPLE) throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
||||||
|
|
||||||
await iap.setup();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const appleRes = await iap.validate(iap.APPLE, plan.additionalData);
|
const details = await findSubscriptionPurchase(plan.additionalData, false);
|
||||||
|
if (!details.isCanceled && !details.isExpired) {
|
||||||
const isValidated = iap.isValidated(appleRes);
|
|
||||||
if (!isValidated) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
|
||||||
|
|
||||||
const purchases = iap.getPurchaseData(appleRes);
|
|
||||||
if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
|
||||||
let newestDate;
|
|
||||||
let newestPurchase;
|
|
||||||
|
|
||||||
for (const purchaseData of purchases) {
|
|
||||||
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
|
||||||
if (!newestDate || datePurchased > newestDate) {
|
|
||||||
newestDate = datePurchased;
|
|
||||||
newestPurchase = purchaseData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iap.isCanceled(newestPurchase) && !iap.isExpired(newestPurchase)) {
|
|
||||||
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
await payments.cancelSubscription({
|
await payments.cancelSubscription({
|
||||||
user,
|
user,
|
||||||
nextBill: new Date(Number(newestPurchase.expirationDate)),
|
nextBill: new Date(Number(details.expirationDate)),
|
||||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,6 +72,53 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
|||||||
return googleRes;
|
return googleRes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function findSubscriptionPurchase (additionalData) {
|
||||||
|
const googleRes = await iap.validate(iap.GOOGLE, additionalData);
|
||||||
|
|
||||||
|
const isValidated = iap.isValidated(googleRes);
|
||||||
|
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||||
|
|
||||||
|
const purchases = iap.getPurchaseData(googleRes);
|
||||||
|
if (purchases.length === 0) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||||
|
|
||||||
|
let purchase;
|
||||||
|
let newestDate;
|
||||||
|
|
||||||
|
for (const i in purchases) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(purchases, i)) {
|
||||||
|
const thisPurchase = purchases[i];
|
||||||
|
const purchaseDate = new Date(Number(thisPurchase.startTimeMillis));
|
||||||
|
if (!newestDate || purchaseDate > newestDate) {
|
||||||
|
newestDate = purchaseDate;
|
||||||
|
purchase = purchases[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
purchase,
|
||||||
|
isCanceled: iap.isCanceled(purchase),
|
||||||
|
isExpired: iap.isExpired(purchase),
|
||||||
|
expirationDate: new Date(Number(purchase.expirationDate)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
api.getSubscriptionPaymentDetails = async function getDetails (userId, subscriptionPlan) {
|
||||||
|
if (!subscriptionPlan || !subscriptionPlan.additionalData) {
|
||||||
|
throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
||||||
|
}
|
||||||
|
const details = await findSubscriptionPurchase(subscriptionPlan.additionalData);
|
||||||
|
return {
|
||||||
|
customerId: details.purchase.purchaseToken,
|
||||||
|
originalPurchaseDate: new Date(Number(details.purchase.startTimeMillis)),
|
||||||
|
expirationDate: details.isCanceled || details.isExpired ? details.expirationDate : null,
|
||||||
|
nextPaymentDate: details.isCanceled || details.isExpired ? null : details.expirationDate,
|
||||||
|
productId: details.purchase.productId,
|
||||||
|
transactionId: details.purchase.orderId,
|
||||||
|
isCanceled: details.isCanceled,
|
||||||
|
isExpired: details.isExpired,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
api.subscribe = async function subscribe (
|
api.subscribe = async function subscribe (
|
||||||
sku,
|
sku,
|
||||||
user,
|
user,
|
||||||
@@ -213,22 +260,11 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
|||||||
let dateTerminated;
|
let dateTerminated;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const googleRes = await iap.validate(iap.GOOGLE, plan.additionalData);
|
const details = await findSubscriptionPurchase(plan.additionalData);
|
||||||
|
if (!details.isCanceled && !details.isExpired) {
|
||||||
const isValidated = iap.isValidated(googleRes);
|
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||||
if (!isValidated) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
|
||||||
|
|
||||||
const purchases = iap.getPurchaseData(googleRes);
|
|
||||||
if (purchases.length === 0) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
|
|
||||||
for (const i in purchases) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(purchases, i)) {
|
|
||||||
const purchase = purchases[i];
|
|
||||||
if (purchase.autoRenewing !== false) return;
|
|
||||||
if (!dateTerminated || Number(purchase.expirationDate) > Number(dateTerminated)) {
|
|
||||||
dateTerminated = new Date(Number(purchase.expirationDate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
dateTerminated = details.expirationDate;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Status:410 means that the subsctiption isn't active anymore and we can safely delete it
|
// Status:410 means that the subsctiption isn't active anymore and we can safely delete it
|
||||||
if (err && err.message === 'Status:410') {
|
if (err && err.message === 'Status:410') {
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ async function addSubToGroupUser (member, group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save unused hourglass and mystery items
|
// save unused hourglass and mystery items
|
||||||
plan.perkMonthCount = memberPlan.perkMonthCount;
|
|
||||||
plan.consecutive.trinkets = memberPlan.consecutive.trinkets;
|
plan.consecutive.trinkets = memberPlan.consecutive.trinkets;
|
||||||
plan.mysteryItems = memberPlan.mysteryItems;
|
plan.mysteryItems = memberPlan.mysteryItems;
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,51 @@ api.subscribeSuccess = async function subscribeSuccess (options = {}) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
api.getSubscriptionPaymentDetails = async function getSubscriptionPaymentDetails (options = {}) {
|
||||||
|
const { user, groupId } = options;
|
||||||
|
let customerId;
|
||||||
|
if (groupId) {
|
||||||
|
const groupFields = basicGroupFields.concat(' purchased');
|
||||||
|
const group = await Group.getGroup({
|
||||||
|
user, groupId, populateLeader: false, groupFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
throw new NotFound(i18n.t('groupNotFound'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.leader !== user._id) {
|
||||||
|
throw new NotAuthorized(i18n.t('onlyGroupLeaderCanManageSubscription'));
|
||||||
|
}
|
||||||
|
customerId = group.purchased.plan.customerId;
|
||||||
|
} else {
|
||||||
|
customerId = user.purchased.plan.customerId;
|
||||||
|
}
|
||||||
|
if (!customerId) throw new NotAuthorized(i18n.t('missingSubscription'));
|
||||||
|
|
||||||
|
const customer = await this.paypalBillingAgreementGet(customerId);
|
||||||
|
if (!customer) throw new NotFound(i18n.t('subscriptionNotFound'));
|
||||||
|
|
||||||
|
console.log('PayPal subscription details:', customer);
|
||||||
|
return {
|
||||||
|
customerId: customer.id,
|
||||||
|
originalPurchaseDate: customer.start_date,
|
||||||
|
expirationDate: customer.agreement_details.ended_at
|
||||||
|
? customer.agreement_details.ended_at
|
||||||
|
: null,
|
||||||
|
nextPaymentDate: customer.agreement_details.next_billing_date
|
||||||
|
? customer.agreement_details.next_billing_date
|
||||||
|
: null,
|
||||||
|
lastPaymentDate: customer.agreement_details.last_payment_date
|
||||||
|
? customer.agreement_details.last_payment_date
|
||||||
|
: null,
|
||||||
|
productId: customer.description,
|
||||||
|
transactionId: customer.id,
|
||||||
|
isCanceled: customer.agreement_details.state === 'Inactive',
|
||||||
|
failedPayments: customer.agreement_details.failed_payment_count,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel a PayPal Subscription
|
* Cancel a PayPal Subscription
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -33,6 +33,26 @@ export async function checkSubData (sub, isGroup = false, coupon) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSubscriptionPaymentDetails (user) {
|
||||||
|
const stripeApi = getStripeApi();
|
||||||
|
|
||||||
|
const { plan } = user.purchased;
|
||||||
|
const customer = await stripeApi.customers.retrieve(plan.customerId);
|
||||||
|
const paymentIntents = await stripeApi.paymentIntents.search({
|
||||||
|
query: `customer:'${plan.customerId}'`,
|
||||||
|
});
|
||||||
|
const lastPayment = paymentIntents.data.length > 0
|
||||||
|
? paymentIntents.data[0]
|
||||||
|
: null;
|
||||||
|
console.log(paymentIntents.data);
|
||||||
|
console.log(customer);
|
||||||
|
return {
|
||||||
|
customerId: customer.id,
|
||||||
|
originalPurchaseDate: new Date(Number(customer.created) * 1000),
|
||||||
|
lastPaymentDate: new Date(Number(lastPayment.created) * 1000),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function applySubscription (session) {
|
export async function applySubscription (session) {
|
||||||
const { metadata, customer: customerId, subscription: subscriptionId } = session;
|
const { metadata, customer: customerId, subscription: subscriptionId } = session;
|
||||||
const {
|
const {
|
||||||
|
|||||||
33
website/server/libs/worker.js
Normal file
33
website/server/libs/worker.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import got from 'got';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
|
const EMAIL_SERVER = {
|
||||||
|
url: nconf.get('EMAIL_SERVER_URL'),
|
||||||
|
auth: {
|
||||||
|
user: nconf.get('EMAIL_SERVER_AUTH_USER'),
|
||||||
|
password: nconf.get('EMAIL_SERVER_AUTH_PASSWORD'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function sendJob (type, config) {
|
||||||
|
const { data, options } = config;
|
||||||
|
const usedOptions = {
|
||||||
|
backoff: { delay: 10 * 60 * 1000, type: 'exponential' },
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
return got.post(`${EMAIL_SERVER.url}/job`, {
|
||||||
|
retry: 5, // retry the http request to the email server 5 times
|
||||||
|
timeout: 60000, // wait up to 60s before timing out
|
||||||
|
username: EMAIL_SERVER.auth.user,
|
||||||
|
password: EMAIL_SERVER.auth.password,
|
||||||
|
json: {
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
options: usedOptions,
|
||||||
|
},
|
||||||
|
}).json().catch(err => logger.error(err, {
|
||||||
|
extraMessage: 'Error while sending an email.',
|
||||||
|
}));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user