mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 05:07:22 +01:00
Compare commits
46 Commits
fiz/stats-
...
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);
|
||||
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: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
@@ -234,7 +234,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
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: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
@@ -254,7 +254,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType, variables);
|
||||
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: {
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
|
||||
@@ -12,11 +12,33 @@ const { i18n } = common;
|
||||
describe('Apple Payments', () => {
|
||||
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', () => {
|
||||
let sku; let user; let token; let receipt; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
|
||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
@@ -25,13 +47,9 @@ describe('Apple Payments', () => {
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
@@ -42,12 +60,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
@@ -209,9 +221,6 @@ describe('Apple Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
|
||||
let paymentsCreateSubscritionStub; let
|
||||
iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -223,12 +232,10 @@ describe('Apple Payments', () => {
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
user = new User();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||
@@ -250,10 +257,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.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 = [
|
||||
{
|
||||
sku: 'subscription1month',
|
||||
@@ -574,8 +600,7 @@ describe('Apple Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
@@ -584,8 +609,7 @@ describe('Apple Payments', () => {
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub.restore();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
@@ -593,8 +617,8 @@ describe('Apple Payments', () => {
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
@@ -606,13 +630,7 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
paymentCancelSubscriptionSpy.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
@@ -695,6 +713,8 @@ describe('Apple Payments', () => {
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapIsCanceledStub).to.be.calledOnce;
|
||||
expect(iapIsExpiredStub).to.be.calledOnce;
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
|
||||
@@ -11,12 +11,36 @@ const { i18n } = common;
|
||||
|
||||
describe('Google Payments', () => {
|
||||
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', () => {
|
||||
let sku; let user; let token; let receipt; let signature; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentBuySkuStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
@@ -25,21 +49,7 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
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 () => {
|
||||
@@ -160,8 +170,7 @@ describe('Google Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentsCreateSubscritionStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -173,19 +182,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -243,7 +245,7 @@ describe('Google Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let signature; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -253,17 +255,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
@@ -276,9 +273,6 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
@@ -308,6 +302,8 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
@@ -332,11 +328,20 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
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');
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ expirationDate: laterDate, autoRenewing: false },
|
||||
.returns([{
|
||||
startTimeMillis: expirationDate.valueOf(),
|
||||
expirationDate,
|
||||
autoRenewing: false,
|
||||
}, {
|
||||
startTimeMillis: laterDate.valueOf(),
|
||||
expirationDate: laterDate,
|
||||
autoRenewing: false,
|
||||
},
|
||||
]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
@@ -365,7 +370,12 @@ describe('Google Payments', () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.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(iapValidateStub).to.be.calledOnce;
|
||||
@@ -388,8 +398,12 @@ describe('Google Payments', () => {
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ autoRenewing: true },
|
||||
{ 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(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
watch: {
|
||||
userIdentifier () {
|
||||
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;
|
||||
if (users.length === 1) {
|
||||
this.loadUser(users[0]._id);
|
||||
|
||||
@@ -5,6 +5,19 @@
|
||||
class="row"
|
||||
>
|
||||
<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
|
||||
:user-id="hero._id"
|
||||
:auth="hero.auth"
|
||||
@@ -96,6 +109,53 @@
|
||||
:reset-counter="resetCounter"
|
||||
@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>
|
||||
@@ -148,6 +208,7 @@ import CustomizationsOwned from './customizationsOwned.vue';
|
||||
import Achievements from './achievements.vue';
|
||||
import UserHistory from './userHistory.vue';
|
||||
import Stats from './stats.vue';
|
||||
import deleteIcon from '@/assets/svg/delete.svg?raw';
|
||||
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
@@ -184,6 +245,11 @@ export default {
|
||||
hasParty: false,
|
||||
partyNotExistError: false,
|
||||
adminHasPrivForParty: true,
|
||||
deleteHabiticaAccount: true,
|
||||
deleteAmplitudeData: true,
|
||||
icons: Object.freeze({
|
||||
deleteIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -249,6 +315,25 @@ export default {
|
||||
|
||||
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) {
|
||||
for (const index in comparisons) {
|
||||
if (index && comparisons[index]) {
|
||||
|
||||
@@ -22,9 +22,6 @@
|
||||
>
|
||||
<p v-if="partyNotExistError">
|
||||
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
|
||||
v-if="questErrors"
|
||||
@@ -37,7 +34,11 @@
|
||||
Party ID
|
||||
</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>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -63,6 +64,13 @@
|
||||
</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
v-if="!userIsPartyLeader"
|
||||
class="btn btn-warning mr-2"
|
||||
@click="makePartyLeader()"
|
||||
>
|
||||
Make Party Leader
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-danger"
|
||||
@click="removeFromParty()"
|
||||
@@ -284,8 +292,6 @@ function resetData (self) {
|
||||
|
||||
if (self.partyNotExistError) {
|
||||
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)
|
||||
@@ -329,13 +335,17 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
userIsPartyLeader: false,
|
||||
questStatus: '',
|
||||
questErrors: '',
|
||||
errorsOrWarningsExist: false,
|
||||
expand: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
userIsPartyLeader () {
|
||||
return this.groupPartyData.leader === this.userId;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
@@ -352,6 +362,14 @@ export default {
|
||||
reloadData: true,
|
||||
});
|
||||
},
|
||||
async makePartyLeader () {
|
||||
await this.$store.dispatch('guilds:update', {
|
||||
group: {
|
||||
id: this.groupPartyData._id,
|
||||
leader: this.userId,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -15,6 +15,25 @@
|
||||
:class="{ 'open': expand }"
|
||||
>
|
||||
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
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
@@ -46,7 +65,7 @@
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="groupPlan">
|
||||
<option value="Group Plan">
|
||||
Group Plan
|
||||
</option>
|
||||
<option value="Stripe">
|
||||
@@ -116,20 +135,10 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<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>
|
||||
<formRow
|
||||
v-model="hero.purchased.plan.customerId"
|
||||
label="Customer ID"
|
||||
/>
|
||||
<div
|
||||
v-if="hero.purchased.plan.planId === 'group_plan_auto'"
|
||||
class="form-group row"
|
||||
@@ -154,7 +163,11 @@
|
||||
>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
@@ -175,177 +188,88 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<formRow
|
||||
v-if="hero.purchased.plan.dateCreated"
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Creation date:
|
||||
</label>
|
||||
<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-model="hero.purchased.plan.dateCreated"
|
||||
label="Creation date"
|
||||
:suffix="dateFormat(hero.purchased.plan.dateCreated)"
|
||||
/>
|
||||
<formRow
|
||||
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">
|
||||
Current sub start date:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="hero.purchased.plan.dateCurrentTypeCreated"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<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"
|
||||
<template #suffix>
|
||||
<strong class="input-group-text">
|
||||
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||
</strong>
|
||||
<a
|
||||
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId && !isGroupPlanMember"
|
||||
v-b-modal.sub_termination_modal
|
||||
class="btn btn-danger"
|
||||
href="#"
|
||||
>
|
||||
Terminate
|
||||
</a>
|
||||
</template>
|
||||
<template
|
||||
v-if="isSubscribed() && !isCancelled()"
|
||||
#helpText
|
||||
>
|
||||
<span class="text-success">
|
||||
The subscription does not have a termination date and is active.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Cumulative months:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.cumulativeCount"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
</span>
|
||||
</template>
|
||||
</formRow>
|
||||
<formRow
|
||||
v-model="hero.purchased.plan.cumulativeCount"
|
||||
label="Cumulative months"
|
||||
input-type="number"
|
||||
help-text="Cumulative subscribed months across subscription periods."
|
||||
/>
|
||||
<formRow
|
||||
v-model="hero.purchased.plan.extraMonths"
|
||||
label="Extra months"
|
||||
input-type="number"
|
||||
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">
|
||||
Cumulative subscribed months across subscription periods.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Extra months:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="hero.purchased.plan.extraMonths"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<a
|
||||
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0"
|
||||
class="btn btn-warning"
|
||||
@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>
|
||||
Apply Credit
|
||||
</a>
|
||||
</template>
|
||||
</formRow>
|
||||
<formRow
|
||||
v-model="hero.purchased.plan.hourglassPromoReceived"
|
||||
label="Received hourglass bonus"
|
||||
:suffix="dateFormat(hero.purchased.plan.hourglassPromoReceived)"
|
||||
/>
|
||||
<formRow
|
||||
v-model="hero.purchased.plan.consecutive.trinkets"
|
||||
label="Mystic Hourglasses"
|
||||
input-type="number"
|
||||
min="0"
|
||||
/>
|
||||
<formRow
|
||||
v-model="hero.purchased.plan.consecutive.gemCapExtra"
|
||||
label="Gem cap increase"
|
||||
input-type="number"
|
||||
min="0"
|
||||
max="26"
|
||||
step="2"
|
||||
/>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Total Gem cap:
|
||||
@@ -354,21 +278,13 @@
|
||||
{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 24 }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Gems bought this month:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.gemsBought"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="hero.purchased.plan.consecutive.gemCapExtra + 24"
|
||||
step="1"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<formRow
|
||||
v-model="hero.purchased.plan.gemsBought"
|
||||
label="Gems bought this month"
|
||||
input-type="number"
|
||||
min="0"
|
||||
:max="hero.purchased.plan.consecutive.gemCapExtra + 24"
|
||||
/>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Mystery Items:
|
||||
@@ -391,7 +307,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'"
|
||||
v-if="!isConvertingToGroupPlan && !isGroupPlanMember"
|
||||
class="form-group row"
|
||||
>
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
@@ -419,6 +335,79 @@
|
||||
>
|
||||
</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
|
||||
v-if="expand"
|
||||
@@ -474,17 +463,36 @@
|
||||
<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;
|
||||
.form-group {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<script>
|
||||
@@ -494,10 +502,61 @@ import { getPlanContext } from '@/../../common/script/cron';
|
||||
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
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 {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
FormRow,
|
||||
},
|
||||
mixins: [saveHero],
|
||||
props: {
|
||||
@@ -520,6 +579,7 @@ export default {
|
||||
isConvertingToGroupPlan: false,
|
||||
groupPlanID: '',
|
||||
subscriptionBlocks,
|
||||
paymentDetails: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -553,6 +613,12 @@ export default {
|
||||
}
|
||||
return terminationDate;
|
||||
},
|
||||
playOrdersUrl () {
|
||||
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
|
||||
},
|
||||
isGroupPlanMember () {
|
||||
return this.hero.purchased.plan.planId === 'group_plan_auto';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dateFormat (date) {
|
||||
@@ -583,6 +649,20 @@ export default {
|
||||
this.isConvertingToGroupPlan = true;
|
||||
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) {
|
||||
e.preventDefault();
|
||||
if (this.isConvertingToGroupPlan) {
|
||||
@@ -601,6 +681,30 @@ export default {
|
||||
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>
|
||||
|
||||
@@ -226,7 +226,7 @@ export default {
|
||||
}
|
||||
},
|
||||
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.questInviteResponses = history.questInviteResponses;
|
||||
this.cron = history.cron;
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
>
|
||||
{{ $t('adminPanel') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasPermission(user, 'groupSupport')"
|
||||
class="nav-link"
|
||||
:to="{name: 'groupAdmin'}"
|
||||
>
|
||||
{{ $t('groupAdmin') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasPermission(user, 'accessControl')"
|
||||
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 () {
|
||||
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 AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
|
||||
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');
|
||||
|
||||
// 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',
|
||||
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);
|
||||
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 * as adminPanel from './adminPanel';
|
||||
import * as admin from './admin';
|
||||
import * as common from './common';
|
||||
import * as user from './user';
|
||||
import * as tasks from './tasks';
|
||||
@@ -26,7 +26,7 @@ import * as blockers from './blockers';
|
||||
// Example: fetch in user.js -> 'user:fetch'
|
||||
|
||||
const actions = flattenAndNamespace({
|
||||
adminPanel,
|
||||
admin,
|
||||
common,
|
||||
user,
|
||||
tasks,
|
||||
|
||||
@@ -36,7 +36,7 @@ const envVars = [
|
||||
'TIME_TRAVEL_ENABLED',
|
||||
'DEBUG_ENABLED',
|
||||
'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 = {};
|
||||
|
||||
@@ -3,5 +3,9 @@
|
||||
"siteBlockers": "Site Blockers",
|
||||
"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.",
|
||||
"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) {
|
||||
hero.purchased.plan.extraMonths = plan.extraMonths;
|
||||
}
|
||||
if (plan.customerId) {
|
||||
if (plan.customerId || plan.customerId === '') {
|
||||
hero.purchased.plan.customerId = plan.customerId;
|
||||
}
|
||||
if (plan.paymentMethod) {
|
||||
if (plan.paymentMethod || plan.customerId === '') {
|
||||
hero.purchased.plan.paymentMethod = plan.paymentMethod;
|
||||
}
|
||||
if (plan.planId) {
|
||||
if (plan.planId || plan.customerId === '') {
|
||||
hero.purchased.plan.planId = plan.planId;
|
||||
}
|
||||
if (plan.owner) {
|
||||
if (plan.owner || plan.customerId === '') {
|
||||
hero.purchased.plan.owner = plan.owner;
|
||||
}
|
||||
if (plan.hourglassPromoReceived) {
|
||||
@@ -341,8 +341,7 @@ api.updateHero = {
|
||||
const group = await Group.getGroup({ user: hero, groupId: groupID });
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.hasNotCancelled()) {
|
||||
hero.purchased.plan.customerId = null;
|
||||
hero.purchased.plan.paymentMethod = null;
|
||||
hero.purchased.plan.paymentMethod = 'groupPlan';
|
||||
await addSubToGroupUser(hero, group);
|
||||
await group.updateGroupPlan();
|
||||
} else {
|
||||
@@ -352,34 +351,34 @@ api.updateHero = {
|
||||
}
|
||||
|
||||
if (updateData.stats) {
|
||||
if (updateData.stats.hp) {
|
||||
if (updateData.stats.hp || updateData.stats.hp === 0) {
|
||||
hero.stats.hp = updateData.stats.hp;
|
||||
}
|
||||
if (updateData.stats.mp) {
|
||||
if (updateData.stats.mp || updateData.stats.mp === 0) {
|
||||
hero.stats.mp = updateData.stats.mp;
|
||||
}
|
||||
if (updateData.stats.exp) {
|
||||
if (updateData.stats.exp || updateData.stats.exp === 0) {
|
||||
hero.stats.exp = updateData.stats.exp;
|
||||
}
|
||||
if (updateData.stats.gp) {
|
||||
if (updateData.stats.gp || updateData.stats.gp === 0) {
|
||||
hero.stats.gp = updateData.stats.gp;
|
||||
}
|
||||
if (updateData.stats.lvl) {
|
||||
if (updateData.stats.lvl || updateData.stats.lvl === 0) {
|
||||
hero.stats.lvl = updateData.stats.lvl;
|
||||
}
|
||||
if (updateData.stats.points) {
|
||||
if (updateData.stats.points || updateData.stats.points === 0) {
|
||||
hero.stats.points = updateData.stats.points;
|
||||
}
|
||||
if (updateData.stats.str) {
|
||||
if (updateData.stats.str || updateData.stats.str === 0) {
|
||||
hero.stats.str = updateData.stats.str;
|
||||
}
|
||||
if (updateData.stats.int) {
|
||||
if (updateData.stats.int || updateData.stats.int === 0) {
|
||||
hero.stats.int = updateData.stats.int;
|
||||
}
|
||||
if (updateData.stats.per) {
|
||||
if (updateData.stats.per || updateData.stats.per === 0) {
|
||||
hero.stats.per = updateData.stats.per;
|
||||
}
|
||||
if (updateData.stats.con) {
|
||||
if (updateData.stats.con || updateData.stats.con === 0) {
|
||||
hero.stats.con = updateData.stats.con;
|
||||
}
|
||||
if (updateData.stats.buffs) {
|
||||
@@ -511,13 +510,22 @@ api.updateHero = {
|
||||
const savedHero = await hero.save();
|
||||
|
||||
if (updateData.removeFromParty) {
|
||||
await leaveGroup({
|
||||
user: savedHero,
|
||||
groupId: savedHero.party._id,
|
||||
res,
|
||||
keep: false,
|
||||
keepChallenges: false,
|
||||
});
|
||||
try {
|
||||
await leaveGroup({
|
||||
user: savedHero,
|
||||
groupId: savedHero.party._id,
|
||||
res,
|
||||
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();
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import validator from 'validator';
|
||||
import merge from 'lodash/merge';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as UserHistory } from '../../models/userHistory';
|
||||
import { model as Group } from '../../models/group';
|
||||
import { model as Blocker } from '../../models/blocker';
|
||||
import {
|
||||
NotFound,
|
||||
} 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 = {};
|
||||
|
||||
@@ -40,8 +48,6 @@ api.searchHero = {
|
||||
|
||||
const { userIdentifier } = req.params;
|
||||
|
||||
const re = new RegExp(String.raw`^${userIdentifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
|
||||
let query;
|
||||
let users = [];
|
||||
if (validator.isUUID(userIdentifier)) {
|
||||
@@ -54,7 +60,7 @@ api.searchHero = {
|
||||
'auth.facebook.emails.value',
|
||||
];
|
||||
for (const field of emailFields) {
|
||||
const emailQuery = { [field]: userIdentifier };
|
||||
const emailQuery = { [field]: userIdentifier.toLowerCase() };
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const found = await User.findOne(emailQuery)
|
||||
.select('contributor backer profile auth')
|
||||
@@ -65,6 +71,7 @@ api.searchHero = {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const re = new RegExp(String.raw`^${userIdentifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
query = { 'auth.local.lowerCaseUsername': { $regex: re, $options: 'i' } };
|
||||
}
|
||||
|
||||
@@ -76,7 +83,8 @@ api.searchHero = {
|
||||
.lean()
|
||||
.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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { sendJob } from '../../libs/worker';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||
import { TransactionModel as Transaction } from '../../models/transaction';
|
||||
@@ -5,9 +6,9 @@ import { TransactionModel as Transaction } from '../../models/transaction';
|
||||
const api = {};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v4/user/purchase-history Get users purchase history
|
||||
* @apiName UserGetPurchaseHistory
|
||||
* @apiGroup User
|
||||
* @api {get} /api/v4/members/:memberId/purchase-history Get members purchase history
|
||||
* @apiName MemberGetPurchaseHistory
|
||||
* @apiGroup Member
|
||||
*
|
||||
*/
|
||||
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;
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import nconf from 'nconf';
|
||||
import got from 'got';
|
||||
import { TAVERN_ID } from '../models/group'; // eslint-disable-line import/no-cycle
|
||||
import { encrypt } from './encryption';
|
||||
import logger from './logger';
|
||||
import common from '../../common';
|
||||
import { sendJob } from './worker';
|
||||
|
||||
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');
|
||||
|
||||
export function getUserInfo (user, fields = []) {
|
||||
@@ -156,29 +148,14 @@ export async function sendTxn (mailingInfoArray, emailType, variables, personalV
|
||||
}
|
||||
|
||||
if (IS_PROD && mailingInfoArray.length > 0) {
|
||||
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: 'email',
|
||||
data: {
|
||||
emailType,
|
||||
to: mailingInfoArray,
|
||||
variables,
|
||||
personalVariables,
|
||||
},
|
||||
options: {
|
||||
priority: 'high',
|
||||
attempts: 5,
|
||||
backoff: { delay: 10 * 60 * 1000, type: 'fixed' },
|
||||
},
|
||||
return sendJob('email', {
|
||||
data: {
|
||||
emailType,
|
||||
to: mailingInfoArray,
|
||||
variables,
|
||||
personalVariables,
|
||||
},
|
||||
}).json().catch(err => logger.error(err, {
|
||||
extraMessage: 'Error while sending an email.',
|
||||
emailType,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -74,7 +74,7 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
||||
return appleRes;
|
||||
};
|
||||
|
||||
api.subscribe = async function subscribe (user, receipt, headers, nextPaymentProcessing) {
|
||||
async function findSubscriptionPurchase (receipt, onlyActive = true) {
|
||||
await iap.setup();
|
||||
|
||||
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) {
|
||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||
}
|
||||
|
||||
let purchase;
|
||||
let newestDate;
|
||||
|
||||
for (const purchaseData of purchaseDataList) {
|
||||
const datePurchased = new Date(Number(purchaseData.purchaseDate));
|
||||
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
||||
if ((!newestDate || datePurchased > newestDate) && dateTerminated > new Date()) {
|
||||
purchase = purchaseData;
|
||||
newestDate = datePurchased;
|
||||
let datePurchased;
|
||||
if (purchaseData.purchaseDate instanceof Date) {
|
||||
datePurchased = purchaseData.purchaseDate;
|
||||
} else {
|
||||
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;
|
||||
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) {
|
||||
const { plan } = user.purchased;
|
||||
|
||||
if (plan.paymentMethod !== api.constants.PAYMENT_METHOD_APPLE) throw new NotAuthorized(shared.i18n.t('missingSubscription'));
|
||||
|
||||
await iap.setup();
|
||||
|
||||
try {
|
||||
const appleRes = await iap.validate(iap.APPLE, plan.additionalData);
|
||||
|
||||
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)) {
|
||||
const details = await findSubscriptionPurchase(plan.additionalData, false);
|
||||
if (!details.isCanceled && !details.isExpired) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||
}
|
||||
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
nextBill: new Date(Number(newestPurchase.expirationDate)),
|
||||
nextBill: new Date(Number(details.expirationDate)),
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||
headers,
|
||||
});
|
||||
|
||||
@@ -72,6 +72,53 @@ api.verifyPurchase = async function verifyPurchase (options) {
|
||||
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 (
|
||||
sku,
|
||||
user,
|
||||
@@ -213,22 +260,11 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
|
||||
let dateTerminated;
|
||||
|
||||
try {
|
||||
const googleRes = await iap.validate(iap.GOOGLE, plan.additionalData);
|
||||
|
||||
const isValidated = iap.isValidated(googleRes);
|
||||
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));
|
||||
}
|
||||
}
|
||||
const details = await findSubscriptionPurchase(plan.additionalData);
|
||||
if (!details.isCanceled && !details.isExpired) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
|
||||
}
|
||||
dateTerminated = details.expirationDate;
|
||||
} catch (err) {
|
||||
// Status:410 means that the subsctiption isn't active anymore and we can safely delete it
|
||||
if (err && err.message === 'Status:410') {
|
||||
|
||||
@@ -180,7 +180,6 @@ async function addSubToGroupUser (member, group) {
|
||||
}
|
||||
|
||||
// save unused hourglass and mystery items
|
||||
plan.perkMonthCount = memberPlan.perkMonthCount;
|
||||
plan.consecutive.trinkets = memberPlan.consecutive.trinkets;
|
||||
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
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
const { metadata, customer: customerId, subscription: subscriptionId } = session;
|
||||
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