diff --git a/database/src/seeds/contributionLink/ContributionLinkInterface.ts b/database/src/seeds/contributionLink/ContributionLinkInterface.ts new file mode 100644 index 000000000..15ba4b72d --- /dev/null +++ b/database/src/seeds/contributionLink/ContributionLinkInterface.ts @@ -0,0 +1,7 @@ +export interface ContributionLinkInterface { + amount: number + name: string + memo: string + validFrom?: Date + validTo?: Date +} diff --git a/database/src/seeds/contributionLink/index.ts b/database/src/seeds/contributionLink/index.ts new file mode 100644 index 000000000..41d28eb60 --- /dev/null +++ b/database/src/seeds/contributionLink/index.ts @@ -0,0 +1,18 @@ +import { ContributionLinkInterface } from './ContributionLinkInterface' + +export const contributionLinks: ContributionLinkInterface[] = [ + { + name: 'Dokumenta 2017', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2017', + amount: 200, + validFrom: new Date(2017, 3, 8), + validTo: new Date(2017, 6, 16), + }, + { + name: 'Dokumenta 2022', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022', + amount: 200, + validFrom: new Date(2022, 5, 18), + validTo: new Date(2022, 8, 25), + }, +] diff --git a/database/src/seeds/creation/CreationInterface.ts b/database/src/seeds/creation/CreationInterface.ts new file mode 100644 index 000000000..ee450bd93 --- /dev/null +++ b/database/src/seeds/creation/CreationInterface.ts @@ -0,0 +1,9 @@ +export interface CreationInterface { + email: string + amount: number + memo: string + contributionDate: string + confirmed?: boolean + // number of months to move the confirmed creation to the past + moveCreationDate?: number +} diff --git a/database/src/seeds/creation/index.ts b/database/src/seeds/creation/index.ts new file mode 100644 index 000000000..cdb123dcb --- /dev/null +++ b/database/src/seeds/creation/index.ts @@ -0,0 +1,155 @@ +import { nMonthsBefore } from '../factory/creation' + +import { CreationInterface } from './CreationInterface' + +const bobsSendings = [ + { + amount: 10, + memo: 'Herzlich Willkommen bei Gradido!', + }, + { + amount: 10, + memo: 'für deine Hilfe, Betty', + }, + { + amount: 23.37, + memo: 'für deine Hilfe, David', + }, + { + amount: 47, + memo: 'für deine Hilfe, Frau Holle', + }, + { + amount: 1.02, + memo: 'für deine Hilfe, Herr Müller', + }, + { + amount: 5.67, + memo: 'für deine Hilfe, Maier', + }, + { + amount: 72.93, + memo: 'für deine Hilfe, Elsbeth', + }, + { + amount: 5.6, + memo: 'für deine Hilfe, Daniel', + }, + { + amount: 8.87, + memo: 'für deine Hilfe, Yoda', + }, + { + amount: 7.56, + memo: 'für deine Hilfe, Sabine', + }, + { + amount: 7.89, + memo: 'für deine Hilfe, Karl', + }, + { + amount: 8.9, + memo: 'für deine Hilfe, Darth Vader', + }, + { + amount: 56.79, + memo: 'für deine Hilfe, Luci', + }, + { + amount: 3.45, + memo: 'für deine Hilfe, Hanne', + }, + { + amount: 8.74, + memo: 'für deine Hilfe, Luise', + }, + { + amount: 7.85, + memo: 'für deine Hilfe, Annegred', + }, + { + amount: 32.7, + memo: 'für deine Hilfe, Prinz von Zamunda', + }, + { + amount: 44.2, + memo: 'für deine Hilfe, Charly Brown', + }, + { + amount: 38.17, + memo: 'für deine Hilfe, Michael', + }, + { + amount: 5.72, + memo: 'für deine Hilfe, Kaja', + }, + { + amount: 3.99, + memo: 'für deine Hilfe, Maja', + }, + { + amount: 4.5, + memo: 'für deine Hilfe, Martha', + }, + { + amount: 8.3, + memo: 'für deine Hilfe, Ursula', + }, + { + amount: 2.9, + memo: 'für deine Hilfe, Urs', + }, + { + amount: 4.6, + memo: 'für deine Hilfe, Mecedes', + }, + { + amount: 74.1, + memo: 'für deine Hilfe, Heidi', + }, + { + amount: 4.5, + memo: 'für deine Hilfe, Peter', + }, + { + amount: 5.8, + memo: 'für deine Hilfe, Fräulein Rottenmeier', + }, +] +const bobsTransactions: CreationInterface[] = [] +bobsSendings.forEach((sending) => { + bobsTransactions.push({ + email: 'bob@baumeister.de', + amount: sending.amount, + memo: sending.memo, + contributionDate: nMonthsBefore(new Date()), + confirmed: true, + }) +}) + +export const creations: CreationInterface[] = [ + { + email: 'bibi@bloxberg.de', + amount: 1000, + memo: 'Herzlich Willkommen bei Gradido!', + contributionDate: nMonthsBefore(new Date()), + confirmed: true, + moveCreationDate: 12, + }, + { + email: 'bibi@bloxberg.de', + amount: 1000, + memo: '#Hexen', + contributionDate: nMonthsBefore(new Date()), + confirmed: true, + moveCreationDate: 12, + }, + ...bobsTransactions, + { + email: 'raeuber@hotzenplotz.de', + amount: 1000, + memo: 'Herzlich Willkommen bei Gradido!', + contributionDate: nMonthsBefore(new Date()), + confirmed: true, + }, +] diff --git a/database/src/seeds/factory/contributionLink.ts b/database/src/seeds/factory/contributionLink.ts new file mode 100644 index 000000000..031fa9c00 --- /dev/null +++ b/database/src/seeds/factory/contributionLink.ts @@ -0,0 +1,33 @@ +import Decimal from 'decimal.js-light' +import { ContributionLink } from '../../entity' + +import { ContributionLinkInterface } from '../contributionLink/ContributionLinkInterface' +import { transactionLinkCode } from './transactionLink' +import { ContributionCycleType } from '../../enum' + +export const contributionLinkFactory = async ( + contributionLink: ContributionLinkInterface, +): Promise => { + return createContributionLink(contributionLink) +} + +export async function createContributionLink(contributionLinkData: ContributionLinkInterface): Promise { + const contributionLink = new ContributionLink() + contributionLink.amount = new Decimal(contributionLinkData.amount) + contributionLink.name = contributionLinkData.name + contributionLink.memo = contributionLinkData.memo + contributionLink.createdAt = new Date() + contributionLink.code = transactionLinkCode(new Date()) + contributionLink.cycle = ContributionCycleType.ONCE + if (contributionLinkData.validFrom) { + contributionLink.validFrom = contributionLinkData.validFrom + } + if (contributionLinkData.validTo) { + contributionLink.validTo = contributionLinkData.validTo + } + contributionLink.maxAmountPerMonth = new Decimal(200) + contributionLink.maxPerCycle = 1 + + return contributionLink.save() +} + \ No newline at end of file diff --git a/database/src/seeds/factory/creation.ts b/database/src/seeds/factory/creation.ts new file mode 100644 index 000000000..ddf5224de --- /dev/null +++ b/database/src/seeds/factory/creation.ts @@ -0,0 +1,76 @@ +import { Contribution, User } from '../../entity' +import { Decimal } from 'decimal.js-light' +import { CreationInterface } from '../creation/CreationInterface' +import { ContributionType, ContributionStatus, TransactionTypeId } from '../../enum' +import { findUserByIdentifier } from '../../queries' +import { createTransaction } from './transaction' + +export function nMonthsBefore(date: Date, months = 1): string { + return new Date(date.getFullYear(), date.getMonth() - months, 1).toISOString() +} + +export async function creationFactory( + creation: CreationInterface, + user?: User | null, + moderatorUser?: User | null, +): Promise { + if (!user) { + user = await findUserByIdentifier(creation.email) + } + if (!user) { + throw new Error(`User ${creation.email} not found`) + } + let contribution = await createContribution(creation, user) + if (creation.confirmed) { + if (!moderatorUser) { + moderatorUser = await findUserByIdentifier('peter@lustig.de') + } + if (!moderatorUser) { + throw new Error('Moderator user not found') + } + contribution = await confirmTransaction(contribution, moderatorUser) + } + return contribution +} + +export async function createContribution(creation: CreationInterface, user: User, store: boolean = true): Promise { + const contribution = new Contribution() + contribution.user = user + contribution.userId = user.id + contribution.amount = new Decimal(creation.amount) + contribution.createdAt = new Date() + contribution.contributionDate = getContributionDate(creation) + contribution.memo = creation.memo + contribution.contributionType = ContributionType.USER + contribution.contributionStatus = ContributionStatus.PENDING + + return store ? contribution.save() : contribution +} + +export async function confirmTransaction(contribution: Contribution, moderatorUser: User, store: boolean = true): Promise { + const now = new Date() + const transaction = await createTransaction( + contribution.amount, + contribution.memo, + contribution.user, + moderatorUser, + TransactionTypeId.CREATION, + now, + contribution.contributionDate, + true, + ) + contribution.confirmedAt = now + contribution.confirmedBy = moderatorUser.id + contribution.transactionId = transaction.id + contribution.transaction = transaction + contribution.contributionStatus = ContributionStatus.CONFIRMED + + return store ? contribution.save() : contribution +} + +function getContributionDate(creation: CreationInterface): Date { + if (creation.moveCreationDate) { + return new Date(nMonthsBefore(new Date(creation.contributionDate), creation.moveCreationDate)) + } + return new Date(creation.contributionDate) +} \ No newline at end of file diff --git a/database/src/seeds/factory/transaction.ts b/database/src/seeds/factory/transaction.ts new file mode 100644 index 000000000..1782c4f18 --- /dev/null +++ b/database/src/seeds/factory/transaction.ts @@ -0,0 +1,55 @@ +import Decimal from 'decimal.js-light' +import { User, Transaction } from '../../entity' +import { TransactionTypeId } from '../../enum' +import { fullName } from 'shared' +import { getLastTransaction } from '../../queries' +import { calculateDecay, Decay } from 'shared' + +export async function createTransaction( + amount: Decimal, + memo: string, + user: User, + linkedUser: User, + type: TransactionTypeId, + balanceDate: Date, + creationDate?: Date, + store: boolean = true, +): Promise { + + const lastTransaction = await getLastTransaction(user.id) + // balance and decay calculation + let newBalance = new Decimal(0) + let decay: Decay | null = null + if (lastTransaction) { + decay = calculateDecay( + lastTransaction.balance, + lastTransaction.balanceDate, + balanceDate, + ) + newBalance = decay.balance + } + newBalance = newBalance.add(amount.toString()) + + const transaction = new Transaction() + transaction.typeId = type + transaction.memo = memo + transaction.userId = user.id + transaction.userGradidoID = user.gradidoID + transaction.userName = fullName(user.firstName, user.lastName) + transaction.userCommunityUuid = user.communityUuid + transaction.linkedUserId = linkedUser.id + transaction.linkedUserGradidoID = linkedUser.gradidoID + transaction.linkedUserName = fullName(linkedUser.firstName, linkedUser.lastName) + transaction.linkedUserCommunityUuid = linkedUser.communityUuid + transaction.previous = lastTransaction ? lastTransaction.id : null + transaction.amount = amount + if (creationDate) { + transaction.creationDate = creationDate + } + transaction.balance = newBalance + transaction.balanceDate = balanceDate + transaction.decay = decay ? decay.decay : new Decimal(0) + transaction.decayStart = decay ? decay.start : null + + return store ? transaction.save() : transaction +} \ No newline at end of file diff --git a/database/src/seeds/factory/transactionLink.ts b/database/src/seeds/factory/transactionLink.ts new file mode 100644 index 000000000..82155a908 --- /dev/null +++ b/database/src/seeds/factory/transactionLink.ts @@ -0,0 +1,59 @@ +import { TransactionLinkInterface } from '../transactionLink/TransactionLinkInterface' +import { TransactionLink } from '../../entity' +import { Decimal } from 'decimal.js-light' +import { findUserByIdentifier } from '../../queries' +import { compoundInterest } from 'shared' +import { randomBytes } from 'node:crypto' + +export async function transactionLinkFactory( + transactionLinkData: TransactionLinkInterface, + userId?: number, +): Promise { + if (!userId) { + const user = await findUserByIdentifier(transactionLinkData.email) + if (!user) { + throw new Error(`User ${transactionLinkData.email} not found`) + } + userId = user.id + } + return createTransactionLink(transactionLinkData, userId) +} + +export async function createTransactionLink(transactionLinkData: TransactionLinkInterface, userId: number, store: boolean = true): Promise { + const holdAvailableAmount = compoundInterest(new Decimal(transactionLinkData.amount.toString()), CODE_VALID_DAYS_DURATION * 24 * 60 * 60) + let createdAt = transactionLinkData.createdAt || new Date() + const validUntil = transactionLinkExpireDate(createdAt) + + const transactionLink = new TransactionLink() + transactionLink.userId = userId + transactionLink.amount = new Decimal(transactionLinkData.amount) + transactionLink.memo = transactionLinkData.memo + transactionLink.holdAvailableAmount = holdAvailableAmount + transactionLink.code = transactionLinkCode(createdAt) + transactionLink.createdAt = createdAt + transactionLink.validUntil = validUntil + + if (transactionLinkData.deletedAt) { + transactionLink.deletedAt = new Date(createdAt.getTime() + 1000) + } + + return store ? transactionLink.save() : transactionLink +} + +////// Transaction Link BUSINESS LOGIC ////// +// TODO: move business logic to shared +export const CODE_VALID_DAYS_DURATION = 14 + +export const transactionLinkExpireDate = (date: Date): Date => { + const validUntil = new Date(date) + return new Date(validUntil.setDate(date.getDate() + CODE_VALID_DAYS_DURATION)) +} + +export const transactionLinkCode = (date: Date): string => { + const time = date.getTime().toString(16) + return ( + randomBytes(12) + .toString('hex') + .substring(0, 24 - time.length) + time + ) +} \ No newline at end of file diff --git a/database/src/seeds/factory/user.ts b/database/src/seeds/factory/user.ts index 3772fe66d..f4614587c 100644 --- a/database/src/seeds/factory/user.ts +++ b/database/src/seeds/factory/user.ts @@ -4,8 +4,9 @@ import { v4 } from 'uuid' import { UserContactType, OptInType, PasswordEncryptionType } from 'shared' import { getHomeCommunity } from '../../queries/communities' import random from 'crypto-random-bigint' +import { Community } from '../../entity' -export const userFactory = async (user: UserInterface): Promise => { +export async function userFactory(user: UserInterface, homeCommunity?: Community | null): Promise { let dbUserContact = new UserContact() dbUserContact.email = user.email ?? '' @@ -30,7 +31,9 @@ export const userFactory = async (user: UserInterface): Promise => { dbUser.password = random(64) dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID } - const homeCommunity = await getHomeCommunity() + if (!homeCommunity) { + homeCommunity = await getHomeCommunity() + } if (homeCommunity) { dbUser.community = homeCommunity dbUser.communityUuid = homeCommunity.communityUuid! @@ -41,5 +44,6 @@ export const userFactory = async (user: UserInterface): Promise => { dbUserContact = await dbUserContact.save() dbUser.emailId = dbUserContact.id dbUser.emailContact = dbUserContact + return dbUser.save() } \ No newline at end of file diff --git a/database/src/seeds/index.ts b/database/src/seeds/index.ts new file mode 100644 index 000000000..c2a659e2f --- /dev/null +++ b/database/src/seeds/index.ts @@ -0,0 +1,106 @@ +import { AppDatabase } from '../AppDatabase' +import { clearDatabase } from '../../migration/clear' +import { createCommunity } from './community' +import { userFactory } from './factory/user' +import { users } from './users' +import { datatype, internet, name } from 'faker' +import { creationFactory } from './factory/creation' +import { creations } from './creation' +import { transactionLinkFactory } from './factory/transactionLink' +import { transactionLinks } from './transactionLink' +import { contributionLinkFactory } from './factory/contributionLink' +import { contributionLinks } from './contributionLink' +import { User } from '../entity' +import { TransactionLink } from '../entity' +import { ContributionLink } from '../entity' + +const RANDOM_USER_COUNT = 100 + +async function run() { + const now = new Date() + // clear database, use mysql2 directly, not AppDatabase + await clearDatabase() + + const db = AppDatabase.getInstance() + await db.init() + + // seed home community + const homeCommunity = await createCommunity(false) + console.info('##seed## seeding home community successful...') + + // seed standard users + // start creation of all users in parallel + // put into map for later direct access + const userCreationIndexedByEmail = new Map>() + for (const user of users) { + userCreationIndexedByEmail.set(user.email!, userFactory(user, homeCommunity)) + } + const defaultUsersPromise = Promise.all(userCreationIndexedByEmail.values()).then(() => { + // log message after all users are created + console.info('##seed## seeding all standard users successful...') + }) + + // seed 100 random users + // start creation of all random users in parallel + const randomUsersCreation: Promise[] = [] + for (let i = 0; i < RANDOM_USER_COUNT; i++) { + randomUsersCreation.push(userFactory({ + firstName: name.firstName(), + lastName: name.lastName(), + email: internet.email(), + language: datatype.boolean() ? 'en' : 'de', + }, homeCommunity)) + } + const randomUsersPromise = Promise.all(randomUsersCreation).then(() => { + // log message after all random users are created + console.info(`##seed## seeding ${RANDOM_USER_COUNT} random users successful...`) + }) + + // create Contribution Links + // start creation of all contribution links in parallel + const contributionLinksPromises: Promise[] = [] + for (const contributionLink of contributionLinks) { + contributionLinksPromises.push(contributionLinkFactory(contributionLink)) + } + const contributionLinksPromise = Promise.all(contributionLinksPromises).then(() => { + // log message after all contribution links are created + console.info('##seed## seeding all contributionLinks successful...') + }) + + // create Transaction Links + // start creation of all transaction links in parallel + const transactionLinksPromises: Promise[] = [] + for (const transactionLink of transactionLinks) { + const user = await userCreationIndexedByEmail.get(transactionLink.email)! + transactionLinksPromises.push(transactionLinkFactory(transactionLink, user.id)) + } + const transactionLinksPromise = Promise.all(transactionLinksPromises).then(() => { + // log message after all transaction links are created + console.info('##seed## seeding all transactionLinks successful...') + }) + + // create GDD serial, must be called one after another because seeding don't use semaphore + const moderatorUser = await userCreationIndexedByEmail.get('peter@lustig.de')! + for (const creation of creations) { + const user = await userCreationIndexedByEmail.get(creation.email)! + await creationFactory(creation, user, moderatorUser) + } + + // wait for all promises to be resolved + await Promise.all([ + defaultUsersPromise, + randomUsersPromise, + contributionLinksPromise, + transactionLinksPromise, + ]) + + await db.destroy() + const timeDiffSeconds = (new Date().getTime() - now.getTime()) / 1000 + console.info(`##seed## seeding successful... after ${timeDiffSeconds} seconds`) +} + +run().catch((err) => { + // biome-ignore lint/suspicious/noConsole: no logger present + console.error('error on seeding', err) +}) + diff --git a/database/src/seeds/transactionLink/TransactionLinkInterface.ts b/database/src/seeds/transactionLink/TransactionLinkInterface.ts new file mode 100644 index 000000000..eaacfdf92 --- /dev/null +++ b/database/src/seeds/transactionLink/TransactionLinkInterface.ts @@ -0,0 +1,10 @@ +export interface TransactionLinkInterface { + email: string + amount: number + memo: string + createdAt?: Date + // TODO: for testing + // redeemedAt?: Date + // redeemedBy?: number + deletedAt?: boolean +} diff --git a/database/src/seeds/transactionLink/index.ts b/database/src/seeds/transactionLink/index.ts new file mode 100644 index 000000000..17683b580 --- /dev/null +++ b/database/src/seeds/transactionLink/index.ts @@ -0,0 +1,55 @@ +import { TransactionLinkInterface } from './TransactionLinkInterface' + +export const transactionLinks: TransactionLinkInterface[] = [ + { + email: 'bibi@bloxberg.de', + amount: 19.99, + memo: 'Leider wollte niemand meine Gradidos haben :(', + }, + { + email: 'bibi@bloxberg.de', + amount: 19.99, + memo: `Kein Trick, keine Zauberrei, +bei Gradidio sei dabei!`, + }, + { + email: 'bibi@bloxberg.de', + amount: 19.99, + memo: `Kein Trick, keine Zauberrei, +bei Gradidio sei dabei!`, + }, + { + email: 'bibi@bloxberg.de', + amount: 19.99, + memo: `Kein Trick, keine Zauberrei, +bei Gradidio sei dabei!`, + }, + { + email: 'bibi@bloxberg.de', + amount: 19.99, + memo: `Kein Trick, keine Zauberrei, +bei Gradidio sei dabei!`, + // TODO: for testing + // memo: `Yeah, eingelöst!`, + // redeemedAt: new Date(2022, 2, 2), + // redeemedBy: not null, + }, + { + email: 'bibi@bloxberg.de', + amount: 19.99, + memo: `Kein Trick, keine Zauberrei, +bei Gradidio sei dabei!`, + }, + { + email: 'bibi@bloxberg.de', + amount: 19.99, + memo: `Kein Trick, keine Zauberrei, +bei Gradidio sei dabei!`, + }, + { + email: 'bibi@bloxberg.de', + amount: 19.99, + memo: 'Da habe ich mich wohl etwas übernommen.', + deletedAt: true, + }, +]