Skip to content

Commit

Permalink
subs: support mixing IAP and LN purchases
Browse files Browse the repository at this point in the history
Also add test coverage.

Previously, making an IAP purchase on an account with some remaining
time credit would wipe away the remaining credits they had.

This commit fixes this and adds an automated test to verify this edge
case.

Testing: Unit tests passing
Signed-off-by: Daniel D’Aquino <[email protected]>
Reviewed-by: William Casarin <[email protected]>
Signed-off-by: William Casarin <[email protected]>
  • Loading branch information
danieldaquino authored and jb55 committed Feb 22, 2024
1 parent 9ddfaa6 commit fa566dd
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 14 deletions.
20 changes: 7 additions & 13 deletions src/router_config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { json_response, simple_response, error_response, invalid_request, unauthorized_response } = require('./server_helpers')
const { create_account, get_account_info_payload, check_account, get_account, put_account, get_account_and_user_id, get_user_uuid } = require('./user_management')
const { create_account, get_account_info_payload, check_account, get_account, put_account, get_account_and_user_id, get_user_uuid, bumpy_set_expiry } = require('./user_management')
const handle_translate = require('./translate')
const verify_receipt = require('./app_store_receipt_verifier').verify_receipt
const bodyParser = require('body-parser')
Expand Down Expand Up @@ -117,19 +117,13 @@ function config_router(app) {
return
}

let account = get_account(app, pubkey)
if (!account) {
let result = create_account(app, req.authorized_pubkey, null)

if (result.request_error) {
invalid_request(res, result.request_error)
return
}
account = result.account
const { account: new_account, request_error } = bumpy_set_expiry(app, req.authorized_pubkey, expiry_date)
if (request_error) {
error_response(res, request_error)
return
}

account.expiry = expiry_date
const { user_id } = put_account(app, pubkey, account)

let { account, user_id } = get_account_and_user_id(app, req.authorized_pubkey)
json_response(res, get_account_info_payload(user_id, account))
return
})
Expand Down
33 changes: 32 additions & 1 deletion src/user_management.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,37 @@ function bump_expiry(api, pubkey, expiry_delta) {
return { account: account, request_error: null }
}

/**
* Sets the expiry date to a fixed date, but also bumps the expiry date if there is any time left.
* It also creates the account if it doesn't exist already.
*
* @param {Object} api - The API object
* @param {string} pubkey - The public key of the user, hex encoded
* @param {number} expiry_date - The new expiry date
*/
function bumpy_set_expiry(api, pubkey, expiry_date) {
const account = get_account(api, pubkey)
if (!account) {
// Create account if it doesn't exist already
return create_account(api, pubkey, expiry_date)
}
if (!account.expiry) {
// Set expiry if it doesn't exist already
account.expiry = expiry_date
}
else if (account.expiry < current_time()) {
// Set new expiry if it has already expired
account.expiry = expiry_date
}
else if (account.expiry >= current_time()) {
// Accumulate expiry if it hasn't expired yet
const remaining_time = account.expiry - current_time()
account.expiry = expiry_date + remaining_time
}
put_account(api, pubkey, account)
return { account: account, request_error: null }
}

function get_account_info_payload(subscriber_number, account) {
if (!account)
return null
Expand Down Expand Up @@ -121,4 +152,4 @@ function get_user_uuid(api, pubkey) {
return uuid
}

module.exports = { check_account, create_account, get_account_info_payload, bump_expiry, get_account, put_account, get_account_and_user_id, get_user_id_from_pubkey, get_user_uuid }
module.exports = { check_account, create_account, get_account_info_payload, bump_expiry, get_account, put_account, get_account_and_user_id, get_user_id_from_pubkey, get_user_uuid, bumpy_set_expiry }
47 changes: 47 additions & 0 deletions test/mixed_iap_ln_flow.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use strict";
// @ts-check

const test = require('tap').test;
const { PurpleTestController } = require('./controllers/purple_test_controller.js');
const { PURPLE_ONE_MONTH } = require('../src/invoicing.js');
const { MOCK_ACCOUNT_UUIDS, MOCK_IAP_DATES } = require('./controllers/mock_iap_controller.js');

test('Mixed IAP/LN Flow — Expiry dates should be nicely handled', async (t) => {
// Set things up
const purple_api_controller = await PurpleTestController.new(t);
const user_uuid = MOCK_ACCOUNT_UUIDS[0]
const user_pubkey_1 = purple_api_controller.new_client();
purple_api_controller.set_account_uuid(user_pubkey_1, user_uuid); // Associate the pubkey with the user_uuid on the server

// Buy a one month subscription via LN flow 25 days before buying the IAP
purple_api_controller.set_current_time(MOCK_IAP_DATES[user_uuid].purchase_date - 60 * 60 * 24 * 25); // 25 days before the IAP purchase date
await purple_api_controller.ln_flow_buy_subscription(user_pubkey_1, PURPLE_ONE_MONTH);

// Check expiry
const account_info_response_1 = await purple_api_controller.clients[user_pubkey_1].get_account();
t.same(account_info_response_1.statusCode, 200);
t.same(account_info_response_1.body.expiry, purple_api_controller.current_time() + 30 * 24 * 60 * 60);
t.same(account_info_response_1.body.active, true);

// Fast forward 25 days, to the IAP purchase date
purple_api_controller.set_current_time(MOCK_IAP_DATES[user_uuid].purchase_date);

// Simulate IAP purchase on the iOS side
const receipt_base64 = purple_api_controller.iap.get_iap_receipt_data(user_uuid); // Get the receipt from the iOS side

// Send the receipt to the server to activate the account
const iap_response = await purple_api_controller.clients[user_pubkey_1].send_iap_receipt(user_uuid, receipt_base64);
t.same(iap_response.statusCode, 200);

// Read the account info now
const account_info_response = await purple_api_controller.clients[user_pubkey_1].get_account();
t.same(account_info_response.statusCode, 200);
// This user still had 5 days left on their subscription, so the expiry date should be 5 days after the IAP expiry date
// i.e. The user should not lose any credit for the time they had left on their subscription
t.same(account_info_response.body.expiry, MOCK_IAP_DATES[user_uuid].expiry_date + 5 * 24 * 60 * 60);
t.same(account_info_response.body.active, true);

// TODO: Test other edge cases?

t.end();
});

0 comments on commit fa566dd

Please sign in to comment.