diff --git a/src/api/admin.ts b/src/api/admin.ts index 1724c07..fec059a 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2022 Jonas Lochmann + * Copyright (C) 2019 - 2026 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -228,7 +228,10 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: { error: 'family not found' } - const canDoPurchase = canDoNextPurchase({ fullVersionUntil: parseInt(familyEntry.fullVersionUntil) }) + const canDoPurchase = canDoNextPurchase({ + fullVersionUntil: parseInt(familyEntry.fullVersionUntil), + fullVersionDebts: parseInt(familyEntry.fullVersionDebts), + }) if (!canDoPurchase) { const lastPurchase = await database.purchase.findOne({ diff --git a/src/api/purchase.ts b/src/api/purchase.ts index da2ff4d..93332d1 100644 --- a/src/api/purchase.ts +++ b/src/api/purchase.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2022 Jonas Lochmann + * Copyright (C) 2019 - 2026 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -54,7 +54,10 @@ export const createPurchaseRouter = ({ database, websocket }: { transaction }) - return canDoNextPurchase({ fullVersionUntil: parseInt(familyEntry.fullVersionUntil, 10) }) + return canDoNextPurchase({ + fullVersionUntil: parseInt(familyEntry.fullVersionUntil, 10), + fullVersionDebts: parseInt(familyEntry.fullVersionDebts, 10), + }) }) res.json({ diff --git a/src/database/family.ts b/src/database/family.ts index c58aac1..a0d9a80 100644 --- a/src/database/family.ts +++ b/src/database/family.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2022 Jonas Lochmann + * Copyright (C) 2019 - 2026 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -37,8 +37,13 @@ export interface FamilyAttributesVersion3 { u2fKeysVersion: string } +export interface FamilyAttributesVersion4 { + fullVersionDebts: string +} + export type FamilyAttributes = FamilyAttributesVersion1 & - FamilyAttributesVersion2 & FamilyAttributesVersion3 + FamilyAttributesVersion2 & FamilyAttributesVersion3 & + FamilyAttributesVersion4 export type FamilyModel = Sequelize.Model & FamilyAttributes export type FamilyModelStatic = typeof Sequelize.Model & { @@ -76,10 +81,22 @@ export const attributesVersion3: SequelizeAttributes = } } +export const attributesVersion4: SequelizeAttributes = { + fullVersionDebts: { + type: Sequelize.BIGINT, + allowNull: false, + defaultValue: 0, + validate: { + min: 0 + } + } +} + export const attributes: SequelizeAttributes = { ...attributesVersion1, ...attributesVersion2, - ...attributesVersion3 + ...attributesVersion3, + ...attributesVersion4 } export const createFamilyModel = (sequelize: Sequelize.Sequelize): FamilyModelStatic => sequelize.define('Family', attributes) as FamilyModelStatic diff --git a/src/database/migration/migrations/20260712-add-family-full-version-debts.ts b/src/database/migration/migrations/20260712-add-family-full-version-debts.ts new file mode 100644 index 0000000..751474d --- /dev/null +++ b/src/database/migration/migrations/20260712-add-family-full-version-debts.ts @@ -0,0 +1,39 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2026 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { QueryInterface, Sequelize, Transaction } from 'sequelize' +import { attributesVersion4 as familyAttributes } from '../../family' + +export async function up (queryInterface: QueryInterface, sequelize: Sequelize) { + await sequelize.transaction({ + type: Transaction.TYPES.EXCLUSIVE + }, async (transaction) => { + await queryInterface.addColumn('Families', 'fullVersionDebts', { + ...familyAttributes.fullVersionDebts + }, { + transaction + }) + }) +} + +export async function down (queryInterface: QueryInterface, sequelize: Sequelize) { + await sequelize.transaction({ + type: Transaction.TYPES.EXCLUSIVE + }, async (transaction) => { + await queryInterface.removeColumn('Families', 'fullVersionDebts', { transaction }) + }) +} diff --git a/src/function/parent/create-family.ts b/src/function/parent/create-family.ts index 475a1cf..9dd1d13 100644 --- a/src/function/parent/create-family.ts +++ b/src/function/parent/create-family.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2022 Jonas Lochmann + * Copyright (C) 2019 - 2026 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -82,7 +82,8 @@ export async function createFamily ({ fullVersionUntil: (Date.now() + 1000 * 60 * 60 * 24 * 14).toString(10), hasFullVersion: true, nextServerKeyRequestSeq: '1', - u2fKeysVersion: generateVersionId() + u2fKeysVersion: generateVersionId(), + fullVersionDebts: '0' }, { transaction }) // create parent user diff --git a/src/function/purchase/add-purchase.ts b/src/function/purchase/add-purchase.ts index ed11fdf..e2f2168 100644 --- a/src/function/purchase/add-purchase.ts +++ b/src/function/purchase/add-purchase.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2022 Jonas Lochmann + * Copyright (C) 2019 - 2026 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -57,11 +57,19 @@ export const addPurchase = async ({ database, familyId, type, service, transacti } const previousFullVersionEndTime = familyEntry.fullVersionUntil + const previousFullVersionDebts = parseInt(familyEntry.fullVersionDebts, 10) - const newFullVersionUntil = Math.max(parseInt(familyEntry.fullVersionUntil, 10), Date.now()) + (type === 'year' ? year : month) + const typeDuration = type === 'year' ? year : month - familyEntry.fullVersionUntil = newFullVersionUntil.toString(10) - familyEntry.hasFullVersion = true + if (typeDuration > previousFullVersionDebts) { + const newFullVersionUntil = Math.max(parseInt(familyEntry.fullVersionUntil, 10), Date.now()) + typeDuration - previousFullVersionDebts + + familyEntry.fullVersionUntil = newFullVersionUntil.toString(10) + familyEntry.fullVersionDebts = '0' + familyEntry.hasFullVersion = true + } else { + familyEntry.fullVersionDebts = (previousFullVersionDebts - typeDuration).toString(10) + } await familyEntry.save({ transaction }) @@ -72,7 +80,7 @@ export const addPurchase = async ({ database, familyId, type, service, transacti type, loggedAt: Date.now().toString(10), previousFullVersionEndTime, - newFullVersionEndTime: newFullVersionUntil.toString(10) + newFullVersionEndTime: familyEntry.fullVersionUntil }, { transaction }) diff --git a/src/function/purchase/can-do-next-purchase.ts b/src/function/purchase/can-do-next-purchase.ts index 50f5dbc..5cc08cc 100644 --- a/src/function/purchase/can-do-next-purchase.ts +++ b/src/function/purchase/can-do-next-purchase.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 Jonas Lochmann + * Copyright (C) 2019 - 2026 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -15,6 +15,9 @@ * along with this program. If not, see . */ -export const canDoNextPurchase = ({ fullVersionUntil }: {fullVersionUntil: number}) => ( - fullVersionUntil < (Date.now() + 1000 * 60 * 60 * 24 * 31) // 31 days +export const canDoNextPurchase = ({ fullVersionUntil, fullVersionDebts }: { + fullVersionUntil: number + fullVersionDebts: number +}) => ( + (fullVersionUntil - fullVersionDebts) < (Date.now() + 1000 * 60 * 60 * 24 * 31) // 31 days ) diff --git a/src/function/purchase/require-family-entry.ts b/src/function/purchase/require-family-entry.ts index 2c4abb7..88feaee 100644 --- a/src/function/purchase/require-family-entry.ts +++ b/src/function/purchase/require-family-entry.ts @@ -1,6 +1,6 @@ /* * server component for the TimeLimit App - * Copyright (C) 2019 - 2020 Jonas Lochmann + * Copyright (C) 2019 - 2026 Jonas Lochmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -43,7 +43,7 @@ export const requireFamilyEntry = async ({ database, deviceAuthToken, transactio where: { familyId: deviceEntry.familyId }, - attributes: ['fullVersionUntil'], + attributes: ['fullVersionUntil', 'fullVersionDebts'], transaction }) @@ -52,7 +52,8 @@ export const requireFamilyEntry = async ({ database, deviceAuthToken, transactio } const familyEntry = { - fullVersionUntil: familyEntryUnsafe.fullVersionUntil + fullVersionUntil: familyEntryUnsafe.fullVersionUntil, + fullVersionDebts: familyEntryUnsafe.fullVersionDebts, } return familyEntry