Skip to content

Commit

Permalink
volume pricing option (#384)
Browse files Browse the repository at this point in the history
Co-authored-by: Antoine de Chevigné <antoine@tryethernal.com>
  • Loading branch information
antoinedc and Antoine de Chevigné authored Nov 7, 2024
1 parent 004a284 commit 1af69b5
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 7 deletions.
46 changes: 44 additions & 2 deletions run/api/explorers.js
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,17 @@ router.delete('/:id', authMiddleware, async (req, res, next) => {
return managedError(new Error(`Could not delete explorer.`), req, res);

if (data.cancelSubscription && explorer.stripeSubscription) {
if (explorer.stripeSubscription.stripeId) {
if (explorer.stripeSubscription.stripePlan.capabilities.volumeSubscription) {
const subscription = await stripe.subscriptions.retrieve(explorer.stripeSubscription.stripeId);
const item = subscription.items.data[0];
await stripe.subscriptionItems.update(item.id, {
quantity: item.quantity - 1,
proration_behavior: 'always_invoice'
});
await db.deleteExplorerSubscription(data.user.id, explorer.id);

}
else if (explorer.stripeSubscription.stripeId) {
const subscription = await stripe.subscriptions.retrieve(explorer.stripeSubscription.stripeId);
await stripe.subscriptions.update(subscription.id, {
cancel_at_period_end: true
Expand Down Expand Up @@ -655,13 +665,45 @@ router.post('/', authMiddleware, async (req, res, next) => {

if (stripePlan.capabilities.customStartingBlock)
options['integrityCheckStartBlockNumber'] = data.fromBlock;

if (stripePlan.capabilities.volumeSubscription) {
const stripeSubscription = await db.getUserStripeSubscription(user.id);
let subscription;
if (stripeSubscription) {
subscription = await stripe.subscriptions.retrieve(stripeSubscription.stripeId);
const item = subscription.items.data[0];
await stripe.subscriptionItems.update(item.id, {
quantity: item.quantity + 1,
proration_behavior: 'always_invoice'
});
}
else {
subscription = await stripe.subscriptions.create({
customer: user.stripeCustomerId,
items: [
{ price: stripePlan.stripePriceId, quantity: 1 }
]
});
await db.createUserStripeSubscription(user.id, subscription, stripePlan);
}

if (subscription)
options['subscription'] = {
stripePlanId: stripePlan.id,
stripeId: subscription.id,
cycleEndsAt: new Date(subscription.current_period_end * 1000),
status: subscription.status
};
else
return managedError(new Error(`Couldn't create subscription.`), req, res);
}
}

const explorer = await db.createExplorerFromOptions(user.id, sanitize(options));
if (!explorer)
return managedError(new Error('Could not create explorer.'), req, res);

if (!usingDefaultPlan && stripePlan && req.query.startSubscription) {
if (!usingDefaultPlan && stripePlan && req.query.startSubscription && !options['subscription']) {
let stripeParams = {
customer: user.stripeCustomerId,
items: [
Expand Down
25 changes: 24 additions & 1 deletion run/lib/firebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ const ExplorerFaucet = models.ExplorerFaucet;
const ExplorerV2Dex = models.ExplorerV2Dex;
const V2DexPair = models.V2DexPair;

const createUserStripeSubscription = (userId, stripeSubscription, stripePlan) => {
if (!userId || !stripeSubscription || !stripePlan)
throw new Error('Missing parameter');

return StripeSubscription.create({
userId,
stripeId: stripeSubscription.id,
stripePlanId: stripePlan.id,
status: stripeSubscription.status
});
}

const getUserStripeSubscription = (userId) => {
if (!userId)
throw new Error('Missing parameter');

return StripeSubscription.findOne({
where: { userId }
});
};

const getV2DexPairCount = async (userId, v2DexId) => {
if (!userId || !v2DexId)
throw new Error('Missing parameter');
Expand Down Expand Up @@ -2478,5 +2499,7 @@ module.exports = {
deactivateV2Dex: deactivateV2Dex,
activateV2Dex: activateV2Dex,
deleteV2Dex: deleteV2Dex,
getV2DexPairCount: getV2DexPairCount
getV2DexPairCount: getV2DexPairCount,
getUserStripeSubscription: getUserStripeSubscription,
createUserStripeSubscription: createUserStripeSubscription
};
89 changes: 85 additions & 4 deletions run/tests/api/explorers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const mockSubscriptionRetrieve = jest.fn();
const mockSubscriptionUpdate = jest.fn();
const mockSubscriptionItemDelete = jest.fn();
const mockInvoiceListUpcomingLines = jest.fn();
const mockSubscriptionItemUpdate = jest.fn();

jest.mock('stripe', () => {
return jest.fn().mockImplementation(() => {
return {
Expand All @@ -16,7 +18,8 @@ jest.mock('stripe', () => {
update: mockSubscriptionUpdate
},
subscriptionItems: {
del: mockSubscriptionItemDelete
del: mockSubscriptionItemDelete,
update: mockSubscriptionItemUpdate
},
invoices: {
listUpcomingLines: mockInvoiceListUpcomingLines
Expand Down Expand Up @@ -873,7 +876,7 @@ describe(`DELETE ${BASE_URL}/:id`, () => {
});

it('Should cancel the subscription with stripe and delete the explorer', (done) => {
jest.spyOn(db, 'getExplorerById').mockResolvedValueOnce({ id: 1, stripeSubscription: { stripeId: 'subscriptionId' }});
jest.spyOn(db, 'getExplorerById').mockResolvedValueOnce({ id: 1, stripeSubscription: { stripeId: 'subscriptionId', stripePlan: { slug: 'slug', capabilities: {} }}});
mockSubscriptionRetrieve.mockResolvedValueOnce({ id: 'subscriptionId' });

request.delete(`${BASE_URL}/1?cancelSubscription=true`)
Expand All @@ -886,7 +889,7 @@ describe(`DELETE ${BASE_URL}/:id`, () => {
});

it('Should delete the subscription if no stripe id', (done) => {
jest.spyOn(db, 'getExplorerById').mockResolvedValueOnce({ id: 1, stripeSubscription: {} });
jest.spyOn(db, 'getExplorerById').mockResolvedValueOnce({ id: 1, stripeSubscription: { stripePlan: { slug: 'slug', capabilities: {} }}});

request.delete(`${BASE_URL}/1?cancelSubscription=true`)
.expect(200)
Expand All @@ -909,7 +912,7 @@ describe(`DELETE ${BASE_URL}/:id`, () => {
});

it('Should delete the workspace if the flag is passed', (done) => {
jest.spyOn(db, 'getExplorerById').mockResolvedValueOnce({ id: 1, workspaceId: 1, stripeSubscription: {}});
jest.spyOn(db, 'getExplorerById').mockResolvedValueOnce({ id: 1, workspaceId: 1, stripeSubscription: { stripePlan: { slug: 'slug', capabilities: {} }}});

request.delete(`${BASE_URL}/1?cancelSubscription=true&deleteWorkspace=true`)
.expect(200)
Expand Down Expand Up @@ -1056,6 +1059,84 @@ describe(`POST ${BASE_URL}/:id/settings`, () => {
});

describe(`POST ${BASE_URL}`, () => {
it('Should update a volume subscription', (done) => {
jest.spyOn(db, 'getUser').mockResolvedValueOnce({ id: 1, stripeCustomerId: 'customerId', workspaces: [{ id: 2 }] });
jest.spyOn(db, 'getUserStripeSubscription').mockResolvedValueOnce({ id: '1234' });
jest.spyOn(db, 'getStripePlan').mockResolvedValueOnce({ stripePriceId: 'priceId', public: false, id: 1, capabilities: { volumeSubscription: true }});
jest.spyOn(db, 'createExplorerFromOptions').mockResolvedValueOnce({ id: 1 });

mockSubscriptionRetrieve.mockResolvedValueOnce({
id: '1234',
status: 'active',
current_period_end: 1,
items: {
data: [
{ id: '1234', quantity: 1 }
]
}
});

mockSubscriptionUpdate.mockResolvedValueOnce({
id: '1234',
status: 'active',
current_period_end: 1
});

request.post(BASE_URL)
.send({ data: { rpcServer: 'test.rpc', name: 'explorer', plan: 'slug' }})
.expect(200)
.then(({ body }) => {
expect(mockSubscriptionItemUpdate).toHaveBeenCalledWith('1234',
{ quantity: 2, proration_behavior: 'always_invoice' }
);
expect(db.createExplorerFromOptions).toHaveBeenCalledWith(1, {
rpcServer: 'test.rpc',
name: 'explorer',
networkId: 1,
subscription: {
stripePlanId: 1,
stripeId: '1234',
cycleEndsAt: new Date(1000),
status: 'active'
}
});
expect(body).toEqual({ id: 1 });
done();
});
});

it('Should create a volume subscription', (done) => {
jest.spyOn(db, 'getUser').mockResolvedValueOnce({ id: 1, stripeCustomerId: 'customerId', workspaces: [{ id: 2 }] });
jest.spyOn(db, 'getUserStripeSubscription').mockResolvedValueOnce(null);
jest.spyOn(db, 'getStripePlan').mockResolvedValueOnce({ stripePriceId: 'priceId', public: false, id: 1, capabilities: { volumeSubscription: true }});
jest.spyOn(db, 'createExplorerFromOptions').mockResolvedValueOnce({ id: 1 });

mockSubscriptionCreate.mockResolvedValueOnce({
id: '1234',
status: 'active',
current_period_end: 1
});

request.post(BASE_URL)
.send({ data: { rpcServer: 'test.rpc', name: 'explorer', plan: 'slug' }})
.expect(200)
.then(({ body }) => {
expect(db.createExplorerFromOptions).toHaveBeenCalledWith(1, {
rpcServer: 'test.rpc',
name: 'explorer',
networkId: 1,
subscription: {
stripePlanId: 1,
stripeId: '1234',
cycleEndsAt: new Date(1000),
status: 'active'
}
});
expect(body).toEqual({ id: 1 });
done();
});
});

it('Should create the explorer with a starting block and a geth tracer', (done) => {
jest.spyOn(db, 'getUser').mockResolvedValueOnce({ id: 1, workspaces: [{ id: 2 }] });
jest.spyOn(db, 'getStripePlan').mockResolvedValueOnce({ public: true, id: 1, capabilities: { customStartingBlock: true }});
Expand Down

0 comments on commit 1af69b5

Please sign in to comment.