mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
merge
This commit is contained in:
commit
657c171898
@ -5,7 +5,7 @@ module.exports = {
|
||||
node: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['prettier', '@typescript-eslint', 'type-graphql', 'jest', 'import'],
|
||||
plugins: ['prettier', '@typescript-eslint', 'type-graphql', 'jest', 'import', 'n'],
|
||||
extends: [
|
||||
'standard',
|
||||
'eslint:recommended',
|
||||
@ -18,7 +18,9 @@ module.exports = {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: true,
|
||||
typescript: {
|
||||
project: ['./tsconfig.json', '**/tsconfig.json'],
|
||||
},
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
@ -101,6 +103,45 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'import/prefer-default-export': 'off',
|
||||
// n
|
||||
'n/handle-callback-err': 'error',
|
||||
'n/no-callback-literal': 'error',
|
||||
'n/no-exports-assign': 'error',
|
||||
'n/no-extraneous-import': 'error',
|
||||
'n/no-extraneous-require': 'error',
|
||||
'n/no-hide-core-modules': 'error',
|
||||
'n/no-missing-import': 'off', // not compatible with typescript
|
||||
'n/no-missing-require': 'error',
|
||||
'n/no-new-require': 'error',
|
||||
'n/no-path-concat': 'error',
|
||||
'n/no-process-exit': 'error',
|
||||
'n/no-unpublished-bin': 'error',
|
||||
'n/no-unpublished-import': 'off', // TODO need to exclude seeds
|
||||
'n/no-unpublished-require': 'error',
|
||||
'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
|
||||
'n/no-unsupported-features/es-builtins': 'error',
|
||||
'n/no-unsupported-features/es-syntax': 'error',
|
||||
'n/no-unsupported-features/node-builtins': 'error',
|
||||
'n/process-exit-as-throw': 'error',
|
||||
'n/shebang': 'error',
|
||||
'n/callback-return': 'error',
|
||||
'n/exports-style': 'error',
|
||||
'n/file-extension-in-import': 'off',
|
||||
'n/global-require': 'error',
|
||||
'n/no-mixed-requires': 'error',
|
||||
'n/no-process-env': 'error',
|
||||
'n/no-restricted-import': 'error',
|
||||
'n/no-restricted-require': 'error',
|
||||
'n/no-sync': 'error',
|
||||
'n/prefer-global/buffer': 'error',
|
||||
'n/prefer-global/console': 'error',
|
||||
'n/prefer-global/process': 'error',
|
||||
'n/prefer-global/text-decoder': 'error',
|
||||
'n/prefer-global/text-encoder': 'error',
|
||||
'n/prefer-global/url': 'error',
|
||||
'n/prefer-global/url-search-params': 'error',
|
||||
'n/prefer-promises/dns': 'error',
|
||||
'n/prefer-promises/fs': 'error',
|
||||
},
|
||||
overrides: [
|
||||
// only for ts files
|
||||
@ -121,8 +162,8 @@ module.exports = {
|
||||
'import/unambiguous': 'off',
|
||||
},
|
||||
parserOptions: {
|
||||
tsconfigRootDir: './',
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json', '**/tsconfig.json'],
|
||||
// this is to properly reference the referenced project database without requirement of compiling it
|
||||
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
|
||||
},
|
||||
|
||||
@ -22,10 +22,12 @@ module.exports = {
|
||||
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
|
||||
'@test/(.*)': '<rootDir>/test/$1',
|
||||
'@entity/(.*)':
|
||||
// eslint-disable-next-line n/no-process-env
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../database/entity/$1'
|
||||
: '<rootDir>/../database/build/entity/$1',
|
||||
'@dbTools/(.*)':
|
||||
// eslint-disable-next-line n/no-process-env
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../database/src/$1'
|
||||
: '<rootDir>/../database/build/src/$1',
|
||||
|
||||
@ -56,25 +56,25 @@
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||
"@typescript-eslint/parser": "^4.28.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"apollo-server-testing": "^2.25.2",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.4",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-type-graphql": "^1.0.0",
|
||||
"faker": "^5.5.3",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"jest": "^27.2.4",
|
||||
"klicktipp-api": "^1.0.2",
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier": "^2.3.1",
|
||||
"prettier": "^2.8.7",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "^3.14.0",
|
||||
@ -84,5 +84,8 @@
|
||||
"ignore": [
|
||||
"**/*.test.ts"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import axios from 'axios'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
@ -35,6 +35,7 @@ export enum RIGHTS {
|
||||
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
|
||||
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
|
||||
OPEN_CREATIONS = 'OPEN_CREATIONS',
|
||||
USER = 'USER',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
|
||||
@ -34,6 +34,7 @@ export const ROLE_USER = new Role('user', [
|
||||
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
|
||||
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
|
||||
RIGHTS.OPEN_CREATIONS,
|
||||
RIGHTS.USER,
|
||||
])
|
||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||
/* eslint-disable n/no-process-env */
|
||||
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
@ -4,7 +4,7 @@ import { ArgsType, Field } from 'type-graphql'
|
||||
@ArgsType()
|
||||
export class TransactionSendArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
identifier: string
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
import { User } from '@entity/User'
|
||||
import { AuthChecker } from 'type-graphql'
|
||||
|
||||
@ -47,6 +47,10 @@ export class Transaction {
|
||||
this.linkId = transaction.contribution
|
||||
? transaction.contribution.contributionLinkId
|
||||
: transaction.transactionLinkId || null
|
||||
this.previousBalance =
|
||||
(transaction.previousTransaction &&
|
||||
transaction.previousTransaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN)) ||
|
||||
new Decimal(0)
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
@ -70,6 +74,9 @@ export class Transaction {
|
||||
@Field(() => Date)
|
||||
balanceDate: Date
|
||||
|
||||
@Field(() => Decimal)
|
||||
previousBalance: Decimal
|
||||
|
||||
@Field(() => Decay)
|
||||
decay: Decay
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { GraphQLError } from 'graphql'
|
||||
|
||||
|
||||
@ -87,7 +87,7 @@ export class ContributionMessageResolver {
|
||||
.select('cm')
|
||||
.from(DbContributionMessage, 'cm')
|
||||
.leftJoinAndSelect('cm.user', 'u')
|
||||
.where({ contributionId: contributionId })
|
||||
.where({ contributionId })
|
||||
.orderBy('cm.createdAt', order)
|
||||
.limit(pageSize)
|
||||
.offset((currentPage - 1) * pageSize)
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Resolver, Query, Args, Ctx, Authorized, Arg, Int, Float } from 'type-graphql'
|
||||
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { User } from '@entity/User'
|
||||
@ -27,8 +27,6 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
|
||||
@ -84,7 +82,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'wrong@email.com',
|
||||
identifier: 'wrong@email.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
@ -112,22 +110,20 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'stephen@hawking.uk',
|
||||
identifier: 'stephen@hawking.uk',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The recipient account was deleted')],
|
||||
errors: [new GraphQLError('No user to given contact')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', async () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail('stephen@hawking.uk')
|
||||
expect(logger.error).toBeCalledWith('The recipient account was deleted', user)
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('No user to given contact', 'stephen@hawking.uk')
|
||||
})
|
||||
})
|
||||
|
||||
@ -143,22 +139,23 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'garrick@ollivander.com',
|
||||
identifier: 'garrick@ollivander.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The recipient account is not activated')],
|
||||
errors: [new GraphQLError('No user with this credentials')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', async () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail('garrick@ollivander.com')
|
||||
expect(logger.error).toBeCalledWith('The recipient account is not activated', user)
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No user with this credentials',
|
||||
'garrick@ollivander.com',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -178,7 +175,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'bob@baumeister.de',
|
||||
identifier: 'bob@baumeister.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
@ -202,7 +199,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
@ -226,7 +223,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
|
||||
},
|
||||
@ -250,7 +247,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'testing',
|
||||
},
|
||||
@ -300,7 +297,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: -50,
|
||||
memo: 'testing negative',
|
||||
},
|
||||
@ -323,7 +320,7 @@ describe('send coins', () => {
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 50,
|
||||
memo: 'unrepeatable memo',
|
||||
},
|
||||
@ -380,7 +377,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 10,
|
||||
memo: 'first transaction',
|
||||
},
|
||||
@ -396,7 +393,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 20,
|
||||
memo: 'second transaction',
|
||||
},
|
||||
@ -412,7 +409,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 30,
|
||||
memo: 'third transaction',
|
||||
},
|
||||
@ -428,7 +425,7 @@ describe('send coins', () => {
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
identifier: 'peter@lustig.de',
|
||||
amount: 40,
|
||||
memo: 'fourth transaction',
|
||||
},
|
||||
|
||||
@ -16,7 +16,6 @@ import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionList } from '@model/TransactionList'
|
||||
import { User } from '@model/User'
|
||||
import { TransactionRepository } from '@repository/Transaction'
|
||||
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
@ -35,8 +34,9 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT
|
||||
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
import { getTransactionList } from './util/getTransactionList'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
@ -149,7 +149,6 @@ export const executeTransaction = async (
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.debug(`prepare Email for transaction received...`)
|
||||
await sendTransactionReceivedEmail({
|
||||
firstName: recipient.firstName,
|
||||
lastName: recipient.lastName,
|
||||
@ -210,8 +209,7 @@ export class TransactionResolver {
|
||||
// find transactions
|
||||
// first page can contain 26 due to virtual decay transaction
|
||||
const offset = (currentPage - 1) * pageSize
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
|
||||
const [userTransactions, userTransactionsCount] = await getTransactionList(
|
||||
user.id,
|
||||
pageSize,
|
||||
offset,
|
||||
@ -276,6 +274,7 @@ export class TransactionResolver {
|
||||
firstDate || now,
|
||||
lastDate || now,
|
||||
self,
|
||||
(userTransactions.length && userTransactions[0].balance) || new Decimal(0),
|
||||
),
|
||||
)
|
||||
logger.debug(`transactions=${transactions}`)
|
||||
@ -283,7 +282,7 @@ export class TransactionResolver {
|
||||
}
|
||||
|
||||
// transactions
|
||||
userTransactions.forEach((userTransaction) => {
|
||||
userTransactions.forEach((userTransaction: dbTransaction) => {
|
||||
const linkedUser =
|
||||
userTransaction.typeId === TransactionTypeId.CREATION
|
||||
? communityUser
|
||||
@ -292,6 +291,15 @@ export class TransactionResolver {
|
||||
})
|
||||
logger.debug(`TransactionTypeId.CREATION: transactions=${transactions}`)
|
||||
|
||||
transactions.forEach((transaction: Transaction) => {
|
||||
if (transaction.typeId !== TransactionTypeId.DECAY) {
|
||||
const { balance, previousBalance, amount } = transaction
|
||||
transaction.decay.decay = new Decimal(
|
||||
Number(balance) - Number(amount) - Number(previousBalance),
|
||||
).toDecimalPlaces(2, Decimal.ROUND_HALF_UP)
|
||||
}
|
||||
})
|
||||
|
||||
// Construct Result
|
||||
return new TransactionList(await balanceResolver.balance(context), transactions)
|
||||
}
|
||||
@ -299,10 +307,10 @@ export class TransactionResolver {
|
||||
@Authorized([RIGHTS.SEND_COINS])
|
||||
@Mutation(() => Boolean)
|
||||
async sendCoins(
|
||||
@Args() { email, amount, memo }: TransactionSendArgs,
|
||||
@Args() { identifier, amount, memo }: TransactionSendArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
|
||||
logger.info(`sendCoins(identifier=${identifier}, amount=${amount}, memo=${memo})`)
|
||||
if (amount.lte(0)) {
|
||||
throw new LogError('Amount to send must be positive', amount)
|
||||
}
|
||||
@ -311,13 +319,9 @@ export class TransactionResolver {
|
||||
const senderUser = getUser(context)
|
||||
|
||||
// validate recipient user
|
||||
const recipientUser = await findUserByEmail(email)
|
||||
if (recipientUser.deletedAt) {
|
||||
throw new LogError('The recipient account was deleted', recipientUser)
|
||||
}
|
||||
const emailContact = recipientUser.emailContact
|
||||
if (!emailContact.emailChecked) {
|
||||
throw new LogError('The recipient account is not activated', recipientUser)
|
||||
const recipientUser = await findUserByIdentifier(identifier)
|
||||
if (!recipientUser) {
|
||||
throw new LogError('The recipient user was not found', recipientUser)
|
||||
}
|
||||
|
||||
await executeTransaction(amount, memo, senderUser, recipientUser)
|
||||
|
||||
@ -5,13 +5,13 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
import { User } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
import { v4 as uuidv4, validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
|
||||
import { OptInType } from '@enum/OptInType'
|
||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||
@ -46,7 +46,13 @@ import {
|
||||
unDeleteUser,
|
||||
sendActivationEmail,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries'
|
||||
import {
|
||||
verifyLogin,
|
||||
queryOptIn,
|
||||
searchAdminUsers,
|
||||
searchUsers,
|
||||
user as userQuery,
|
||||
} from '@/seeds/graphql/queries'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
@ -1420,7 +1426,7 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('changes to gradidoID on login', async () => {
|
||||
await mutate({ mutation: login, variables: variables })
|
||||
await mutate({ mutation: login, variables })
|
||||
|
||||
const usercontact = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
@ -1441,7 +1447,7 @@ describe('UserResolver', () => {
|
||||
|
||||
it('can login after password change', async () => {
|
||||
resetToken()
|
||||
expect(await mutate({ mutation: login, variables: variables })).toEqual(
|
||||
expect(await mutate({ mutation: login, variables })).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
login: {
|
||||
@ -2298,6 +2304,124 @@ describe('UserResolver', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws and logs "401 Unauthorized" error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: userQuery,
|
||||
variables: {
|
||||
identifier: 'identifier',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('401 Unauthorized')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
const uuid = uuidv4()
|
||||
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
describe('identifier is no gradido ID and no email', () => {
|
||||
it('throws and logs "Unknown identifier type" error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: userQuery,
|
||||
variables: {
|
||||
identifier: 'identifier',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Unknown identifier type')],
|
||||
}),
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('Unknown identifier type', 'identifier')
|
||||
})
|
||||
})
|
||||
|
||||
describe('identifier is not found', () => {
|
||||
it('throws and logs "No user found to given identifier" error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: userQuery,
|
||||
variables: {
|
||||
identifier: uuid,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('No user found to given identifier')],
|
||||
}),
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('No user found to given identifier', uuid)
|
||||
})
|
||||
})
|
||||
|
||||
describe('identifier is found via email', () => {
|
||||
it('returns user', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: userQuery,
|
||||
variables: {
|
||||
identifier: 'bibi@bloxberg.de',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
user: {
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('identifier is found via gradidoID', () => {
|
||||
it('returns user', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: userQuery,
|
||||
variables: {
|
||||
identifier: user.gradidoID,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
user: {
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
},
|
||||
},
|
||||
errors: undefined,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('printTimeDuration', () => {
|
||||
|
||||
@ -72,6 +72,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time'
|
||||
|
||||
import { FULL_CREATION_AVAILABLE } from './const/const'
|
||||
import { getUserCreations } from './util/creations'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
|
||||
const random = require('random-bigint')
|
||||
@ -97,6 +98,7 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
|
||||
return emailContact
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export const activationLink = (verificationCode: BigInt): string => {
|
||||
logger.debug(`activationLink(${verificationCode})...`)
|
||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
|
||||
@ -819,11 +821,17 @@ export class UserResolver {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.USER])
|
||||
@Query(() => User)
|
||||
async user(@Arg('identifier') identifier: string): Promise<User> {
|
||||
return new User(await findUserByIdentifier(identifier))
|
||||
}
|
||||
}
|
||||
|
||||
export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||
const dbUserContact = await DbUserContact.findOneOrFail(
|
||||
{ email: email },
|
||||
{ email },
|
||||
{ withDeleted: true, relations: ['user'] },
|
||||
).catch(() => {
|
||||
throw new LogError('No user with this credentials', email)
|
||||
@ -834,7 +842,7 @@ export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||
}
|
||||
|
||||
async function checkEmailExists(email: string): Promise<boolean> {
|
||||
const userContact = await DbUserContact.findOne({ email: email }, { withDeleted: true })
|
||||
const userContact = await DbUserContact.findOne({ email }, { withDeleted: true })
|
||||
if (userContact) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers'
|
||||
@ -152,7 +152,7 @@ describe('semaphore', () => {
|
||||
})
|
||||
const bibisTransaction = mutate({
|
||||
mutation: sendCoins,
|
||||
variables: { email: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
|
||||
variables: { identifier: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: login,
|
||||
@ -168,7 +168,7 @@ describe('semaphore', () => {
|
||||
})
|
||||
const bobsTransaction = mutate({
|
||||
mutation: sendCoins,
|
||||
variables: { email: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
|
||||
variables: { identifier: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: login,
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
36
backend/src/graphql/resolver/util/findUserByIdentifier.ts
Normal file
36
backend/src/graphql/resolver/util/findUserByIdentifier.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { validate, version } from 'uuid'
|
||||
|
||||
import LogError from '@/server/LogError'
|
||||
|
||||
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
|
||||
let user: DbUser | undefined
|
||||
if (validate(identifier) && version(identifier) === 4) {
|
||||
user = await DbUser.findOne({ where: { gradidoID: identifier }, relations: ['emailContact'] })
|
||||
if (!user) {
|
||||
throw new LogError('No user found to given identifier', identifier)
|
||||
}
|
||||
} else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) {
|
||||
const userContact = await DbUserContact.findOne(
|
||||
{
|
||||
email: identifier,
|
||||
emailChecked: true,
|
||||
},
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
if (!userContact) {
|
||||
throw new LogError('No user with this credentials', identifier)
|
||||
}
|
||||
if (!userContact.user) {
|
||||
throw new LogError('No user to given contact', identifier)
|
||||
}
|
||||
user = userContact.user
|
||||
user.emailContact = userContact
|
||||
} else {
|
||||
// last is alias when implemented
|
||||
throw new LogError('Unknown identifier type', identifier)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
20
backend/src/graphql/resolver/util/getTransactionList.ts
Normal file
20
backend/src/graphql/resolver/util/getTransactionList.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
|
||||
import { Order } from '@enum/Order'
|
||||
|
||||
export const getTransactionList = async (
|
||||
userId: number,
|
||||
limit: number,
|
||||
offset: number,
|
||||
order: Order,
|
||||
): Promise<[DbTransaction[], number]> => {
|
||||
return DbTransaction.findAndCount({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
order: { balanceDate: order, id: order },
|
||||
relations: ['previousTransaction'],
|
||||
skip: offset,
|
||||
take: limit,
|
||||
})
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { GraphQLScalarType, Kind } from 'graphql'
|
||||
|
||||
|
||||
@ -22,5 +22,5 @@ async function main() {
|
||||
main().catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
throw e
|
||||
})
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { MiddlewareFn } from 'type-graphql'
|
||||
|
||||
import { KlickTipp } from '@model/KlickTipp'
|
||||
@ -28,6 +29,7 @@ export const klicktippNewsletterStateMiddleware: MiddlewareFn = async (
|
||||
{ root, args, context, info },
|
||||
next,
|
||||
) => {
|
||||
// eslint-disable-next-line n/callback-return
|
||||
const result = await next()
|
||||
let klickTipp = new KlickTipp({ status: 'Unsubscribed' })
|
||||
if (CONFIG.KLICKTIPP) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||
|
||||
@ -75,8 +75,8 @@ export const sendActivationEmail = gql`
|
||||
`
|
||||
|
||||
export const sendCoins = gql`
|
||||
mutation ($email: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(email: $email, amount: $amount, memo: $memo)
|
||||
mutation ($identifier: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -340,3 +340,12 @@ export const listContributionMessages = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const user = gql`
|
||||
query ($identifier: String!) {
|
||||
user(identifier: $identifier) {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { backendLogger as logger } from './logger'
|
||||
|
||||
export class LogError extends Error {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
import { configure, getLogger } from 'log4js'
|
||||
|
||||
@ -61,4 +61,5 @@ ${JSON.stringify(requestContext.response.errors, null, 2)}`)
|
||||
}
|
||||
|
||||
export const plugins =
|
||||
// eslint-disable-next-line n/no-process-env
|
||||
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, logPlugin]
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import { EntityRepository, Repository } from '@dbTools/typeorm'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { Order } from '@enum/Order'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
|
||||
@EntityRepository(Transaction)
|
||||
export class TransactionRepository extends Repository<Transaction> {
|
||||
findByUserPaged(
|
||||
userId: number,
|
||||
limit: number,
|
||||
offset: number,
|
||||
order: Order,
|
||||
onlyCreation?: boolean,
|
||||
): Promise<[Transaction[], number]> {
|
||||
const query = this.createQueryBuilder('userTransaction')
|
||||
.leftJoinAndSelect(
|
||||
'userTransaction.contribution',
|
||||
'contribution',
|
||||
'userTransaction.id = contribution.transactionId',
|
||||
)
|
||||
.where('userTransaction.userId = :userId', { userId })
|
||||
|
||||
if (onlyCreation) {
|
||||
query.andWhere('userTransaction.typeId = :typeId', {
|
||||
typeId: TransactionTypeId.CREATION,
|
||||
})
|
||||
}
|
||||
|
||||
return query
|
||||
.orderBy('userTransaction.balanceDate', order)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.getManyAndCount()
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Repository, EntityRepository } from '@dbTools/typeorm'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
@ -38,6 +38,7 @@ const virtualLinkTransaction = (
|
||||
createdAt: Date,
|
||||
validUntil: Date,
|
||||
user: User,
|
||||
previousBalance: Decimal,
|
||||
): Transaction => {
|
||||
const linkDbTransaction: dbTransaction = {
|
||||
id: -2,
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/*
|
||||
Elopage Webhook
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
import { initialize } from '@dbTools/helpers'
|
||||
import { entities } from '@entity/index'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
@ -40,7 +40,6 @@ export const testEnvironment = async (testLogger: any = logger, testI18n: any =
|
||||
const testClient = createTestClient(server.apollo)
|
||||
const mutate = testClient.mutate
|
||||
const query = testClient.query
|
||||
await initialize()
|
||||
return { mutate, query, con }
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -4,5 +4,3 @@ DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
MIGRATIONS_TABLE=migrations
|
||||
|
||||
TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}
|
||||
|
||||
@ -6,5 +6,3 @@ DB_USER=$DB_USER
|
||||
DB_PASSWORD=$DB_PASSWORD
|
||||
DB_DATABASE=gradido_community
|
||||
MIGRATIONS_TABLE=migrations
|
||||
|
||||
TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}
|
||||
|
||||
@ -96,4 +96,8 @@ export class Transaction extends BaseEntity {
|
||||
@OneToOne(() => Contribution, (contribution) => contribution.transaction)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
|
||||
contribution?: Contribution | null
|
||||
|
||||
@OneToOne(() => Transaction)
|
||||
@JoinColumn({ name: 'previous' })
|
||||
previousTransaction?: Transaction | null
|
||||
}
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
const CONFIG = require('./src/config')
|
||||
|
||||
module.export = {
|
||||
name: 'default',
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
seeds: ['src/seeds/**/*{.ts,.js}'],
|
||||
factories: ['src/factories/**/*{.ts,.js}'],
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import CONFIG from './config'
|
||||
import { createPool, PoolConfig } from 'mysql'
|
||||
import { Migration } from 'ts-mysql-migrate'
|
||||
import path from 'path'
|
||||
|
||||
const poolConfig: PoolConfig = {
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
user: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
}
|
||||
|
||||
// Pool?
|
||||
const pool = createPool(poolConfig)
|
||||
|
||||
// Create & Initialize Migrations
|
||||
const migration = new Migration({
|
||||
conn: pool,
|
||||
tableName: CONFIG.MIGRATIONS_TABLE,
|
||||
silent: true,
|
||||
dir: path.join(__dirname, '..', 'migrations'),
|
||||
})
|
||||
|
||||
const initialize = async (): Promise<void> => {
|
||||
await migration.initialize()
|
||||
}
|
||||
|
||||
const resetDB = async (closePool = false): Promise<void> => {
|
||||
await migration.reset() // use for resetting database
|
||||
if (closePool) pool.end()
|
||||
}
|
||||
|
||||
export { resetDB, pool, migration, initialize }
|
||||
@ -1,18 +1,29 @@
|
||||
import 'reflect-metadata'
|
||||
import prepare from './prepare'
|
||||
import connection from './typeorm/connection'
|
||||
import { resetDB, pool, migration } from './helpers'
|
||||
import { createDatabase } from './prepare'
|
||||
import CONFIG from './config'
|
||||
|
||||
import { createPool } from 'mysql'
|
||||
import { Migration } from 'ts-mysql-migrate'
|
||||
import path from 'path'
|
||||
|
||||
const run = async (command: string) => {
|
||||
// Database actions not supported by our migration library
|
||||
await prepare()
|
||||
|
||||
// Database connection for TypeORM
|
||||
const con = await connection()
|
||||
if (!con || !con.isConnected) {
|
||||
throw new Error(`Couldn't open connection to database`)
|
||||
}
|
||||
await createDatabase()
|
||||
|
||||
// Initialize Migrations
|
||||
const pool = createPool({
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
user: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
})
|
||||
const migration = new Migration({
|
||||
conn: pool,
|
||||
tableName: CONFIG.MIGRATIONS_TABLE,
|
||||
silent: true,
|
||||
dir: path.join(__dirname, '..', 'migrations'),
|
||||
})
|
||||
await migration.initialize()
|
||||
|
||||
// Execute command
|
||||
@ -25,14 +36,13 @@ const run = async (command: string) => {
|
||||
break
|
||||
case 'reset':
|
||||
// TODO protect from production
|
||||
await resetDB() // use for resetting database
|
||||
await migration.reset()
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported command ${command}`)
|
||||
}
|
||||
|
||||
// Terminate connections gracefully
|
||||
await con.close()
|
||||
pool.end()
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export interface TransactionContext {
|
||||
typeId: number
|
||||
userId: number
|
||||
balance: Decimal
|
||||
balanceDate: Date
|
||||
amount: Decimal
|
||||
memo: string
|
||||
creationDate?: Date
|
||||
sendReceiverUserId?: number
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
export interface UserContext {
|
||||
pubKey?: Buffer
|
||||
email?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
deletedAt?: Date
|
||||
password?: BigInt
|
||||
privKey?: Buffer
|
||||
emailHash?: Buffer
|
||||
createdAt?: Date
|
||||
emailChecked?: boolean
|
||||
language?: string
|
||||
publisherId?: number
|
||||
passphrase?: string
|
||||
}
|
||||
|
||||
export interface ServerUserContext {
|
||||
username?: string
|
||||
password?: string
|
||||
email?: string
|
||||
role?: string
|
||||
activated?: number
|
||||
lastLogin?: Date
|
||||
created?: Date
|
||||
modified?: Date
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export interface UserInterface {
|
||||
// from user
|
||||
email?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
password?: BigInt
|
||||
pubKey?: Buffer
|
||||
privKey?: Buffer
|
||||
emailHash?: Buffer
|
||||
createdAt?: Date
|
||||
emailChecked?: boolean
|
||||
language?: string
|
||||
deletedAt?: Date
|
||||
publisherId?: number
|
||||
passphrase?: string
|
||||
// from server user
|
||||
serverUserPassword?: string
|
||||
role?: string
|
||||
activated?: number
|
||||
lastLogin?: Date
|
||||
modified?: Date
|
||||
// flag for admin
|
||||
isAdmin?: boolean
|
||||
// flag for balance (creation of 1000 GDD)
|
||||
addBalance?: boolean
|
||||
// balance
|
||||
recordDate?: Date
|
||||
creationDate?: Date
|
||||
amount?: Decimal
|
||||
}
|
||||
@ -1,15 +1,8 @@
|
||||
/* PREPARE SCRIPT
|
||||
*
|
||||
* This file ensures operations our migration library
|
||||
* can not take care of are done.
|
||||
* This applies to all Database Operations like
|
||||
* creating, deleting, renaming Databases.
|
||||
*/
|
||||
import { createConnection } from 'mysql2/promise'
|
||||
|
||||
import { createConnection, RowDataPacket } from 'mysql2/promise'
|
||||
import CONFIG from './config'
|
||||
|
||||
export default async (): Promise<void> => {
|
||||
export const createDatabase = async (): Promise<void> => {
|
||||
const con = await createConnection({
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
@ -25,6 +18,8 @@ export default async (): Promise<void> => {
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
DEFAULT COLLATE utf8mb4_unicode_ci;`)
|
||||
|
||||
/* LEGACY CODE
|
||||
import { RowDataPacket } from 'mysql2/promise'
|
||||
// Check if old migration table is present, delete if needed
|
||||
const [rows] = await con.query(`SHOW TABLES FROM \`${CONFIG.DB_DATABASE}\` LIKE 'migrations';`)
|
||||
if ((<RowDataPacket>rows).length > 0) {
|
||||
@ -37,6 +32,7 @@ export default async (): Promise<void> => {
|
||||
console.log('Found and dropped old migrations table')
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
await con.end()
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import { createConnection, Connection } from 'typeorm'
|
||||
import CONFIG from '../config'
|
||||
import { entities } from '../../entity/index'
|
||||
|
||||
const connection = async (): Promise<Connection | null> => {
|
||||
let con = null
|
||||
try {
|
||||
con = await createConnection({
|
||||
name: 'default',
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
entities,
|
||||
synchronize: false,
|
||||
})
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
return con
|
||||
}
|
||||
|
||||
export default connection
|
||||
@ -4,7 +4,6 @@
|
||||
import CONFIG from '@/config'
|
||||
import connection from '@/typeorm/connection'
|
||||
import { checkDBVersion } from '@/typeorm/DBVersion'
|
||||
import { initialize } from '@dbTools/helpers'
|
||||
import { entities } from '@entity/index'
|
||||
import { logger } from './testSetup'
|
||||
|
||||
@ -42,7 +41,6 @@ export const testEnvironment = async () => {
|
||||
logger.fatal('Fatal: Database Version incorrect')
|
||||
throw new Error('Fatal: Database Version incorrect')
|
||||
}
|
||||
await initialize()
|
||||
return { con }
|
||||
}
|
||||
|
||||
|
||||
@ -26,18 +26,51 @@ Dies ist ein rein technischer Key und wird nur **innerhalb** der Anwendung zur I
|
||||
|
||||
Die GradidoID ist zwar auch ein rein technischer Key, doch wird dieser als eine UUID der Version 4 erstellt. Dies basiert auf einer (pseudo)zufällig generierten Zahl aus 16 Bytes mit einer theoretischen Konfliktfreiheit von  in hexadezimaler Notation nach einem Pattern von fünf Gruppen durch Bindestrich getrennt - z.B. `550e8400-e29b-41d4-a716-446655440000`
|
||||
|
||||
Somit kann die GradidoID auch System übergreifend zwischen Communities ausgetauscht werden und bietet dennoch eine weitestgehende eindeutige theoretisch konfliktfreie Identifikation des Users. System intern ist die Eindeutigkeit bei der Erstellung eines neuen Users auf jedenfall sichergestellt. Sollte ein User den Wechsel von einer Community in eine andere gradido-Community wünschen, so soll falls möglich die GradidoID für den User erhalten bleiben und übernommen werden können. Dies muss beim Umzug in der Ziel-Community geprüft werden. Falls diese GradidoID aus der Quell-Community wider erwarten existieren sollte, dann muss doch einen neue GradidoID für den User erzeugt werden.
|
||||
Somit kann die GradidoID auch System übergreifend zwischen Communities ausgetauscht werden und bietet dennoch eine weitestgehende eindeutige theoretisch konfliktfreie Identifikation des Users. System intern ist die Eindeutigkeit bei der Erstellung eines neuen Users auf jedenfall sichergestellt. Sollte ein User den Wechsel von einer Community in eine andere gradido-Community wünschen, so soll falls möglich die GradidoID für den User erhalten bleiben und übernommen werden können. Dies muss beim Umzug in der Ziel-Community geprüft werden. Falls diese GradidoID aus der Quell-Community wider erwarten existieren sollte, dann muss doch einen neue GradidoID für den User in der Ziel-Community erzeugt werden.
|
||||
|
||||
#### Alias
|
||||
|
||||
Der Alias eines Users ist als rein fachlicher Key ausgelegt, der frei vom User definiert werden kann. Bei der Definition dieses frei definierbaren und menschenlesbaren Schlüsselwertes stellt die Gradido-Anwendung sicher, dass der vom User eingegebene Wert nicht schon von einem anderen User dieser Community verwendet wird. Für die Anlage eines Alias gelten folgende Konventionen:
|
||||
|
||||
- mindestens 5 Zeichen
|
||||
* alphanumerisch
|
||||
* keine Umlaute
|
||||
* nach folgender Regel erlaubt (RegEx: [a-zA-Z0-9]-|_[a-zA-Z0-9])
|
||||
- Blacklist für Schlüsselworte, die frei definiert werden können
|
||||
- vordefinierte/reservierte System relevante Namen dürfen maximal aus 4 Zeichen bestehen
|
||||
* alpha-nummerisch
|
||||
* 2 <= Länge des alias <=20
|
||||
* beginnt mit einem Buchstaben
|
||||
* keine Umlaute
|
||||
* keine Sonderzeichen ausser dem Bindestrich "-" und dem Unterstrich "_"
|
||||
* nicht mehr als 2 Wiederholungen des gleichen Zeichens direkt hintereinander
|
||||
* kein Unterscheidung von Groß-Kleinschreibung, es findet eine Konvertierung auf Kleinschreibung statt
|
||||
|
||||
Blackliste für nicht vom User verwendbare alias-Definitionen:
|
||||
|
||||
Notation: das %-Zeichen dient als Platzhalter für 0 oder beliebig viele der erlaubten Zeichen
|
||||
|
||||
* %gradido% (= die Sequenz gradido darf nicht enthalten sein)
|
||||
* %community% (= die Sequenz community darf nicht enthalten sein)
|
||||
* %communities% (= die Sequenz communities darf nicht enthalten sein)
|
||||
* %admin% (= die Sequenz admin darf nicht enthalten sein)
|
||||
* %gast% (= die Sequenz gast darf nicht enthalten sein)
|
||||
* %guest% (= die Sequenz guest darf nicht enthalten sein)
|
||||
* support% (= darf nicht mit der Sequenz support beginnen)
|
||||
* user% (= darf nicht mit der Sequenz user beginnen)
|
||||
* usr% (= darf nicht mit der Sequenz usr beginnen)
|
||||
* home% (= darf nicht mit der Sequenz home beginnen)
|
||||
* chief% (= darf nicht mit der Sequenz chief beginnen)
|
||||
* chef% (= darf nicht mit der Sequenz chef beginnen)
|
||||
* master% (= darf nicht mit der Sequenz master beginnen)
|
||||
* email% (= darf nicht mit der Sequenz email beginnen)
|
||||
* mail% (= darf nicht mit der Sequenz mail beginnen)
|
||||
* root% (= darf nicht mit der Sequenz root beginnen)
|
||||
* tmp% (= darf nicht mit der Sequenz tmp beginnen)
|
||||
* temp% (= darf nicht mit der Sequenz temp beginnen)
|
||||
* gdd% (= darf nicht mit der Sequenz gdd beginnen)
|
||||
* gdt% (= darf nicht mit der Sequenz gdt beginnen)
|
||||
* gdb% (= darf nicht mit der Sequenz gdb beginnen)
|
||||
* age (= darf nicht age lauten)
|
||||
* gmw (= darf nicht gmw lauten)
|
||||
* auf (= darf nicht auf lauten)
|
||||
* ...
|
||||
|
||||
Um für die Zukunft für eine Community im Rahmen der Dreifachen-Geldschöpfung und deren Verwaltung bestimmte Alias-Werte nicht an allgemeine User zu verlieren, werden in der Blackliste jetzt schon solche Belegungen reserviert. Damit können diese ggf. später wieder für system relevante User der Community wieder freigegeben werden.
|
||||
|
||||
#### Email
|
||||
|
||||
@ -49,7 +82,7 @@ Die Email wird weiterhin als Kommunikationskanal ausserhalb der Gradido-Anwendun
|
||||
|
||||
Die Erfassung des Alias erfolgt als zusätzliche Eingabe direkt bei der Registrierung eines neuen Users oder als weiterer Schritt direkt nach dem Login.
|
||||
|
||||
Dieser UseCase ist in die **Ausbaustufe-1** und **Ausbaustufe-x** unterteilt.
|
||||
Dieser UseCase ist in die **Ausbaustufe-1** und **Ausbaustufe-x** unterteilt.
|
||||
|
||||
Alle beschriebenen Anforderungen der **Ausbaustufe-1** können mit Produktivsetzung des Issues #1798 - [GradidoID 1: adapt and migrate database schema](https://github.com/gradido/gradido/issues/1798) und dem [PR #2058 - GradidoID 1: adapt and migrate database schema](https://github.com/gradido/gradido/pull/2058) umgesetzt werden.
|
||||
|
||||
@ -63,7 +96,7 @@ In der Eingabemaske der Registrierung wird nun zusätzlich das Feld *Alias* ange
|
||||
|
||||

|
||||
|
||||
Mit dem (optionalen ?) Button "Eindeutigkeit prüfen" wird dem User die Möglichkeit gegeben vorab die Eindeutigkeit seiner *Alias*-Eingabe zu verifizieren ohne den Dialog über den "Registrieren"-Button zu verlassen. Denn es muss sichergestellt sein, dass noch kein existierender User der Community genau diesen *Alias* evtl. schon verwendet.
|
||||
Mit dem (optionalen ?) Button "Eindeutigkeit prüfen" wird dem User die Möglichkeit gegeben vorab die Eindeutigkeit seiner *Alias*-Eingabe zu verifizieren ohne den Dialog über den "Registrieren"-Button zu verlassen. Denn es muss sichergestellt sein, dass noch kein existierender User der Community genau diesen *Alias* evtl. schon verwendet.
|
||||
|
||||
Wird diese Prüfung vom User nicht ausgeführt bevor er den Dialog mit dem "Registrieren"-Button abschließt, so erfolgt die *Alias*-Eindeutigkeitsprüfung als erster Schritt bevor die anderen Eingaben als neuer User geprüft und angelegt werden.
|
||||
|
||||
@ -93,20 +126,8 @@ Der Sprung nach der Login-Seite nach erfolgreichem Login auf die Profil-Seite ö
|
||||
Im Eingabe-Modus der Alias-Gruppe hat das Eingabefeld den Fokus und darin wird:
|
||||
|
||||
* wenn noch kein Alias für den User in der Datenbank vorhanden ist, vom System ein Vorschlag unterbreitet. Der Vorschlag basiert auf dem Vornamen des Users und wird durch folgende Logik ermittelt:
|
||||
* es wird mit dem Vorname des Users eine Datenbankabfrage durchgeführt, die zählt, wieviele User-Aliase es schon mit diesem Vornamen gibt und falls notwendig direkt mit einer nachfolgenden Nummer als Postfix versehen sind.
|
||||
* Aufgrund der Konvention, dass ein Alias mindestens 5 Zeichen lang sein muss, sind ggf. führende Nullen mitzuberücksichten.
|
||||
* **Beispiel-1**: *Max* als Vorname
|
||||
* in der Datenbank gibt es schon mehrer User mit den Aliasen: *Maximilian*, *Max01*, *Max_M*, *Max-M*, *MaxMu* und *Max02*.
|
||||
* Dann schlägt das System den Alias *Max03* vor, da *Max* nur 3 Zeichen lang ist und es schon zwei Aliase *Max* gefolgt mit einer Nummer gibt (*Max01* und *Max02*)
|
||||
* Die Aliase *Maximilian*, *Max_M*, *Max-M* und *MaxMu* werden nicht mitgezählt, das diese nach *Max* keine direkt folgende Ziffern haben
|
||||
* **Beispiel-2**: *August* als Vorname
|
||||
* in der Datenbank gibt es schon mehrer User mit den Aliasen: *Augusta*, *Augustus*, *Augustinus*
|
||||
* Dann schlägt das System den Alias *August* vor, da *August* schon 6 Zeichen lang ist und es noch keinen anderen User mit Alias *August* gibt
|
||||
* die Aliase *Augusta*, *Augustus* und *Augustinus* werden nicht mit gezählt, da diese länger als 5 Zeichen sind und sich von *August* unterscheiden
|
||||
* **Beispiel-3**: *Nick* als Vorname
|
||||
* in der Datenbank gibt es schon mehrer User mit den Aliasen: *Nicko*, *Nickodemus*
|
||||
* Dann schlägt das System den Alias *Nick1* vor, da *Nick* kürzer als 5 Zeichen ist und es noch keinen anderen User mit dem Alias *Nick1* gibt
|
||||
* die Aliase *Nicko* und *Nickodemus* werden nicht mit gezählt, da diese länger als 5 Zeichen sind und sich von *Nick* unterscheiden
|
||||
* es wird mit dem Vorname des Users eine Datenbankabfrage durchgeführt, die zählt, wieviele User-Aliase es schon mit diesem Vornamen gibt und falls notwendig direkt mit einer nachfolgenden Nummer als Postfix versehen.
|
||||
* Aufgrund der Konvention für eine Alias-Definition könnte ein Vorname ggf. gegen die Alias-Regeln verstossen oder aber auch evtl. zu kurz oder lang sein. Auch ein mögliches Blockieren durch die Blacklist könnte den Vornamen des Users als alias verhindern. Dann muss der User selbst manuell seinen alias vollständig erfassen ohne, dass das System einen Vorschlag unterbreiten könnte.
|
||||
* wenn schon ein Alias für den User in der Datenbank vorhanden ist, dann wird dieser unverändert aus der Datenbank und ohne Systemvorschlag einfach angezeigt.
|
||||
|
||||
Der User kann nun den im Eingabefeld angezeigten Alias verändern, wobei die Alias-Konventionen, wie oben im ersten Kapitel beschrieben einzuhalten und zu validieren sind.
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">
|
||||
<div>
|
||||
{{ previousBookedBalance | GDD }}
|
||||
{{ previousBalance | GDD }}
|
||||
{{ decay === '0' ? $t('math.minus') : '' }}
|
||||
{{ decay | GDD }} {{ $t('math.equal') }}
|
||||
<b>{{ balance | GDD }}</b>
|
||||
@ -35,7 +35,7 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
previousBookedBalance: {
|
||||
previousBalance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<div>{{ $t('decay.last_transaction') }}</div>
|
||||
</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">
|
||||
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
|
||||
<div>
|
||||
<span>
|
||||
{{ $d(new Date(decay.start), 'long') }}
|
||||
@ -24,30 +24,44 @@
|
||||
</b-row>
|
||||
<duration-row :decayStart="decay.start" :decayEnd="decay.end" />
|
||||
|
||||
<!-- Previous Balance -->
|
||||
<b-row class="mt-2">
|
||||
<b-col cols="12" lg="6" md="3">
|
||||
<div>{{ $t('decay.old_balance') }}</div>
|
||||
</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
|
||||
{{ previousBalance | GDD }}
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<!-- Decay-->
|
||||
<b-row>
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<b-row class="mt-0">
|
||||
<b-col cols="12" lg="3" md="3">
|
||||
<div>{{ $t('decay.decay') }}</div>
|
||||
</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">{{ decay.decay | GDD }}</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
|
||||
{{ decay.decay | GDD }}
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<!-- Type-->
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-row>
|
||||
<b-row class="mb-2">
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys-->
|
||||
<b-col cols="12" lg="4" md="4">{{ $t(`decay.types.${typeId.toLowerCase()}`) }}</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">{{ amount | GDD }}</b-col>
|
||||
<b-col cols="12" lg="3" md="3">{{ $t(`decay.types.${typeId.toLowerCase()}`) }}</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
|
||||
{{ amount | GDD }}
|
||||
</b-col>
|
||||
</b-row>
|
||||
<!-- Total-->
|
||||
<b-row>
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<div>{{ $t('decay.total') }}</div>
|
||||
<b-row class="border-top pt-2">
|
||||
<b-col cols="12" lg="3" md="3">
|
||||
<div>{{ $t('decay.new_balance') }}</div>
|
||||
</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">
|
||||
<b>{{ (Number(amount) + Number(decay.decay)) | GDD }}</b>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
|
||||
<b>{{ balance | GDD }}</b>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
@ -63,6 +77,8 @@ export default {
|
||||
DurationRow,
|
||||
},
|
||||
props: {
|
||||
balance: { type: String, default: '0' },
|
||||
previousBalance: { type: String, default: '0' },
|
||||
amount: { type: String, default: '0' },
|
||||
typeId: { type: String, default: '' },
|
||||
memo: { type: String, default: '' },
|
||||
|
||||
@ -7,7 +7,15 @@
|
||||
:decay="decay"
|
||||
:typeId="typeId"
|
||||
/>
|
||||
<decay-information-long v-else :amount="amount" :decay="decay" :typeId="typeId" :memo="memo" />
|
||||
<decay-information-long
|
||||
v-else
|
||||
:amount="amount"
|
||||
:decay="decay"
|
||||
:typeId="typeId"
|
||||
:memo="memo"
|
||||
:balance="balance"
|
||||
:previousBalance="previousBalance"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -39,6 +47,14 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
balance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
previousBalance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isStartBlock() {
|
||||
|
||||
@ -5,9 +5,7 @@
|
||||
<b-row class="mt-5">
|
||||
<b-col cols="2"></b-col>
|
||||
<b-col>
|
||||
<div class="h4">
|
||||
{{ email }}
|
||||
</div>
|
||||
<div class="h4">{{ userName ? userName : identifier }}</div>
|
||||
<div class="mt-3 h5">{{ $t('form.memo') }}</div>
|
||||
<div>{{ memo }}</div>
|
||||
</b-col>
|
||||
@ -64,9 +62,10 @@ export default {
|
||||
name: 'TransactionConfirmationSend',
|
||||
props: {
|
||||
balance: { type: Number, required: true },
|
||||
email: { type: String, required: false, default: '' },
|
||||
identifier: { type: String, required: false, default: '' },
|
||||
amount: { type: Number, required: true },
|
||||
memo: { type: String, required: true },
|
||||
userName: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -2,9 +2,17 @@ import { mount } from '@vue/test-utils'
|
||||
import TransactionForm from './TransactionForm'
|
||||
import flushPromises from 'flush-promises'
|
||||
import { SEND_TYPES } from '@/pages/Send'
|
||||
import DashboardLayout from '@/layouts/DashboardLayout'
|
||||
import { createMockClient } from 'mock-apollo-client'
|
||||
import VueApollo from 'vue-apollo'
|
||||
import { user as userQuery } from '@/graphql/queries'
|
||||
|
||||
const mockClient = createMockClient()
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: mockClient,
|
||||
})
|
||||
|
||||
const localVue = global.localVue
|
||||
localVue.use(VueApollo)
|
||||
|
||||
describe('TransactionForm', () => {
|
||||
let wrapper
|
||||
@ -22,6 +30,7 @@ describe('TransactionForm', () => {
|
||||
},
|
||||
$route: {
|
||||
params: {},
|
||||
query: {},
|
||||
},
|
||||
}
|
||||
|
||||
@ -34,10 +43,24 @@ describe('TransactionForm', () => {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
provide: DashboardLayout.provide,
|
||||
apolloProvider,
|
||||
})
|
||||
}
|
||||
|
||||
const userQueryMock = jest.fn()
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
userQuery,
|
||||
userQueryMock.mockRejectedValueOnce({ message: 'Query user name fails!' }).mockResolvedValue({
|
||||
data: {
|
||||
user: {
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
@ -139,7 +162,7 @@ describe('TransactionForm', () => {
|
||||
.setValue(' valid@email.com ')
|
||||
await wrapper.find('div[data-test="input-email"]').find('input').trigger('blur')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.form.email).toBe('valid@email.com')
|
||||
expect(wrapper.vm.form.identifier).toBe('valid@email.com')
|
||||
})
|
||||
})
|
||||
|
||||
@ -290,12 +313,12 @@ Die ganze Welt bezwingen.“`)
|
||||
.find('textarea')
|
||||
.setValue('Long enough')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.form.email).toBe('someone@watches.tv')
|
||||
expect(wrapper.vm.form.identifier).toBe('someone@watches.tv')
|
||||
expect(wrapper.vm.form.amount).toBe('87.23')
|
||||
expect(wrapper.vm.form.memo).toBe('Long enough')
|
||||
await wrapper.find('button[type="reset"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.form.email).toBe('')
|
||||
expect(wrapper.vm.form.identifier).toBe('')
|
||||
expect(wrapper.vm.form.amount).toBe('')
|
||||
expect(wrapper.vm.form.memo).toBe('')
|
||||
})
|
||||
@ -321,10 +344,11 @@ Die ganze Welt bezwingen.“`)
|
||||
expect(wrapper.emitted('set-transaction')).toEqual([
|
||||
[
|
||||
{
|
||||
email: 'someone@watches.tv',
|
||||
identifier: 'someone@watches.tv',
|
||||
amount: 87.23,
|
||||
memo: 'Long enough',
|
||||
selected: 'send',
|
||||
userName: '',
|
||||
},
|
||||
],
|
||||
])
|
||||
@ -346,5 +370,26 @@ Die ganze Welt bezwingen.“`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with gradido ID', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
mocks.$route.query.gradidoID = 'gradido-ID'
|
||||
wrapper = Wrapper()
|
||||
await wrapper.vm.$nextTick()
|
||||
})
|
||||
|
||||
describe('query for username with success', () => {
|
||||
it('has no email input field', () => {
|
||||
expect(wrapper.find('div[data-test="input-email"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('queries the username', () => {
|
||||
expect(userQueryMock).toBeCalledWith({
|
||||
identifier: 'gradido-ID',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -50,16 +50,24 @@
|
||||
<b-col>
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<div v-if="radioSelected === sendTypes.send">
|
||||
<div v-if="radioSelected === sendTypes.send && !gradidoID">
|
||||
<input-email
|
||||
:name="$t('form.recipient')"
|
||||
:label="$t('form.recipient')"
|
||||
:placeholder="$t('form.email')"
|
||||
v-model="form.email"
|
||||
v-model="form.identifier"
|
||||
:disabled="isBalanceDisabled"
|
||||
@onValidation="onValidation"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="gradidoID" class="mb-4">
|
||||
<b-row>
|
||||
<b-col>{{ $t('form.recipient') }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col class="font-weight-bold">{{ userName }}</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6">
|
||||
<input-amount
|
||||
@ -121,6 +129,7 @@ import { SEND_TYPES } from '@/pages/Send'
|
||||
import InputEmail from '@/components/Inputs/InputEmail'
|
||||
import InputAmount from '@/components/Inputs/InputAmount'
|
||||
import InputTextarea from '@/components/Inputs/InputTextarea'
|
||||
import { user as userQuery } from '@/graphql/queries'
|
||||
|
||||
export default {
|
||||
name: 'TransactionForm',
|
||||
@ -131,20 +140,20 @@ export default {
|
||||
},
|
||||
props: {
|
||||
balance: { type: Number, default: 0 },
|
||||
email: { type: String, default: '' },
|
||||
identifier: { type: String, default: '' },
|
||||
amount: { type: Number, default: 0 },
|
||||
memo: { type: String, default: '' },
|
||||
selected: { type: String, default: 'send' },
|
||||
},
|
||||
inject: ['getTunneledEmail'],
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
email: this.email,
|
||||
identifier: this.identifier,
|
||||
amount: this.amount ? String(this.amount) : '',
|
||||
memo: this.memo,
|
||||
},
|
||||
radioSelected: this.selected,
|
||||
userName: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -152,33 +161,48 @@ export default {
|
||||
this.$refs.formValidator.validate()
|
||||
},
|
||||
onSubmit() {
|
||||
if (this.gradidoID) this.form.identifier = this.gradidoID
|
||||
this.$emit('set-transaction', {
|
||||
selected: this.radioSelected,
|
||||
email: this.form.email,
|
||||
identifier: this.form.identifier,
|
||||
amount: Number(this.form.amount.replace(',', '.')),
|
||||
memo: this.form.memo,
|
||||
userName: this.userName,
|
||||
})
|
||||
},
|
||||
onReset(event) {
|
||||
event.preventDefault()
|
||||
this.form.email = ''
|
||||
this.form.identifier = ''
|
||||
this.form.amount = ''
|
||||
this.form.memo = ''
|
||||
this.$refs.formValidator.validate()
|
||||
},
|
||||
setNewRecipientEmail() {
|
||||
this.form.email = this.recipientEmail ? this.recipientEmail : this.form.email
|
||||
if (this.$route.query && !this.$route.query === {}) this.$router.replace({ query: undefined })
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
recipientEmail() {
|
||||
this.setNewRecipientEmail()
|
||||
apollo: {
|
||||
UserName: {
|
||||
query() {
|
||||
return userQuery
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
variables() {
|
||||
return { identifier: this.gradidoID }
|
||||
},
|
||||
skip() {
|
||||
return !this.gradidoID
|
||||
},
|
||||
update({ user }) {
|
||||
this.userName = `${user.firstName} ${user.lastName}`
|
||||
},
|
||||
error({ message }) {
|
||||
this.toastError(message)
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
disabled() {
|
||||
if (
|
||||
this.form.email.length > 5 &&
|
||||
this.form.identifier.length > 5 &&
|
||||
parseInt(this.form.amount) <= parseInt(this.balance) &&
|
||||
this.form.memo.length > 5 &&
|
||||
this.form.memo.length <= 255
|
||||
@ -193,15 +217,12 @@ export default {
|
||||
sendTypes() {
|
||||
return SEND_TYPES
|
||||
},
|
||||
recipientEmail() {
|
||||
return this.getTunneledEmail()
|
||||
gradidoID() {
|
||||
return this.$route.query && this.$route.query.gradidoID
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setNewRecipientEmail()
|
||||
},
|
||||
mounted() {
|
||||
if (this.form.email !== '') this.$refs.formValidator.validate()
|
||||
if (this.form.identifier !== '') this.$refs.formValidator.validate()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -93,8 +93,9 @@ describe('GddTransactionList', () => {
|
||||
{
|
||||
id: -1,
|
||||
typeId: 'DECAY',
|
||||
amount: '-0.16778637075575395772595',
|
||||
balance: '31.59320453982945549519405',
|
||||
amount: '-0.16',
|
||||
balance: '31.59',
|
||||
previousBalance: '31.75',
|
||||
balanceDate: '2022-03-03T08:54:54',
|
||||
memo: '',
|
||||
linkedUser: null,
|
||||
@ -110,6 +111,7 @@ describe('GddTransactionList', () => {
|
||||
typeId: 'SEND',
|
||||
amount: '1',
|
||||
balance: '31.76099091058520945292',
|
||||
previousBalance: '30.76',
|
||||
balanceDate: '2022-02-28T13:55:47',
|
||||
memo:
|
||||
'Um den Kessel schlingt den Reihn, Werft die Eingeweid‘ hinein. Kröte du, die Nacht und Tag Unterm kalten Steine lag,',
|
||||
@ -129,6 +131,7 @@ describe('GddTransactionList', () => {
|
||||
typeId: 'RECEIVE',
|
||||
amount: '10',
|
||||
balance: '10',
|
||||
previousBalance: '31.75',
|
||||
balanceDate: '2022-02-23T10:55:30',
|
||||
memo:
|
||||
'Monatlanges Gift sog ein, In den Topf zuerst hinein… (William Shakespeare, Die Hexen aus Macbeth)',
|
||||
@ -148,6 +151,7 @@ describe('GddTransactionList', () => {
|
||||
typeId: 'CREATION',
|
||||
amount: '1000',
|
||||
balance: '32.96482231613347376132',
|
||||
previousBalance: '31.75',
|
||||
balanceDate: '2022-02-25T07:29:26',
|
||||
memo: 'Jammern hilft nichts, sondern ich kann selber meinen Teil dazu beitragen.',
|
||||
linkedUser: {
|
||||
@ -414,6 +418,7 @@ describe('GddTransactionList', () => {
|
||||
return {
|
||||
amount: '3.14',
|
||||
balanceDate: '2021-04-29T17:26:40+00:00',
|
||||
previousBalance: '31.75',
|
||||
decay: {
|
||||
decay: '-477.01',
|
||||
start: '2021-05-13T17:46:31.000Z',
|
||||
|
||||
@ -19,10 +19,7 @@
|
||||
class="pointer bg-white appBoxShadow gradido-border-radius px-4 pt-2 test-list-group-item"
|
||||
>
|
||||
<template #DECAY>
|
||||
<transaction-decay
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
/>
|
||||
<transaction-decay v-bind="transactions[index]" />
|
||||
</template>
|
||||
</transaction-list-item>
|
||||
</div>
|
||||
@ -34,27 +31,15 @@
|
||||
class="pointer mb-3 bg-white appBoxShadow gradido-border-radius p-3 test-list-group-item"
|
||||
>
|
||||
<template #SEND>
|
||||
<transaction-send
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
<transaction-send v-bind="transactions[index]" />
|
||||
</template>
|
||||
|
||||
<template #RECEIVE>
|
||||
<transaction-receive
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
<transaction-receive v-bind="transactions[index]" />
|
||||
</template>
|
||||
|
||||
<template #CREATION>
|
||||
<transaction-creation
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
<transaction-creation v-bind="transactions[index]" />
|
||||
</template>
|
||||
|
||||
<template #LINK_SUMMARY>
|
||||
@ -127,10 +112,6 @@ export default {
|
||||
})
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
previousBookedBalance(idx) {
|
||||
if (this.transactions[idx + 1]) return this.transactions[idx + 1].balance
|
||||
return '0'
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isPaginationVisible() {
|
||||
|
||||
@ -6,12 +6,15 @@ const localVue = global.localVue
|
||||
const propsData = {
|
||||
link: '',
|
||||
}
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
}
|
||||
|
||||
describe('FigureQrCode', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(FigureQrCode, { localVue, propsData })
|
||||
return mount(FigureQrCode, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -19,12 +22,55 @@ describe('FigureQrCode', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the Div Element ".figure-qr-code"', () => {
|
||||
expect(wrapper.find('div.figure-qr-code').exists()).toBeTruthy()
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders the Div Element "q-r-canvas"', () => {
|
||||
expect(wrapper.find('q-r-canvas'))
|
||||
it('has options filled', () => {
|
||||
expect(wrapper.vm.options).toEqual({
|
||||
cellSize: 8,
|
||||
correctLevel: 'H',
|
||||
data: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('renders the Div Element ".figure-qr-code"', () => {
|
||||
expect(wrapper.find('div.figure-qr-code').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the Div Element "qrbox"', () => {
|
||||
expect(wrapper.find('div.qrbox').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the Canvas Element "#qrcanvas"', () => {
|
||||
const canvas = wrapper.find('#qrcanvas')
|
||||
|
||||
expect(canvas.exists()).toBe(true)
|
||||
const canvasEl = canvas.element
|
||||
const canvasWidth = canvasEl.width
|
||||
const canvasHeight = canvasEl.height
|
||||
|
||||
expect(canvasWidth).toBeGreaterThan(0)
|
||||
expect(canvasHeight).toBeGreaterThan(0)
|
||||
|
||||
const canvasContext = canvasEl.toDataURL('image/png')
|
||||
expect(canvasContext).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders the A Element "#download"', () => {
|
||||
const downloadLink = wrapper.find('#download')
|
||||
expect(downloadLink.exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('Download QR-Code link', () => {
|
||||
beforeEach(() => {
|
||||
const downloadLink = wrapper.find('#download')
|
||||
downloadLink.trigger('click')
|
||||
})
|
||||
|
||||
it('click the A Element "#download" set an href', () => {
|
||||
expect(wrapper.find('#download').attributes('href')).toEqual('data:image/png;base64,00')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,18 @@
|
||||
<template>
|
||||
<div class="figure-qr-code">
|
||||
<div class="qrbox">
|
||||
<q-r-canvas :options="options" class="canvas" />
|
||||
<div>
|
||||
<q-r-canvas :options="options" class="canvas mb-3" id="qrcanvas" ref="canvas" />
|
||||
</div>
|
||||
<a
|
||||
id="download"
|
||||
ref="download"
|
||||
download="GradidoLinkQRCode.png"
|
||||
href=""
|
||||
@click="downloadImg(this)"
|
||||
>
|
||||
{{ $t('download') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -37,6 +48,13 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
downloadImg() {
|
||||
const canvas = this.$refs.canvas.$el
|
||||
const image = canvas.toDataURL('image/png')
|
||||
this.$refs.download.href = image
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@ -32,11 +32,7 @@
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div class="font-weight-bold">
|
||||
<name
|
||||
:linkedUser="transaction.linkedUser"
|
||||
v-on="$listeners"
|
||||
fontColor="text-dark"
|
||||
/>
|
||||
<name :linkedUser="transaction.linkedUser" fontColor="text-dark" />
|
||||
</div>
|
||||
<div class="d-flex mt-3">
|
||||
<div class="small">
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<div>{{ $t('decay.past_time') }}</div>
|
||||
</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">
|
||||
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
|
||||
<span v-if="duration">{{ durationText }}</span>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
@ -3,9 +3,11 @@ import Name from './Name'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$router: {
|
||||
push: jest.fn(),
|
||||
push: routerPushMock,
|
||||
history: {
|
||||
current: {
|
||||
fullPath: '/transactions',
|
||||
@ -47,7 +49,7 @@ describe('Name', () => {
|
||||
describe('with linked user', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setProps({
|
||||
linkedUser: { firstName: 'Bibi', lastName: 'Bloxberg', email: 'bibi@bloxberg.de' },
|
||||
linkedUser: { firstName: 'Bibi', lastName: 'Bloxberg', gradidoID: 'gradido-ID' },
|
||||
})
|
||||
})
|
||||
|
||||
@ -64,13 +66,17 @@ describe('Name', () => {
|
||||
await wrapper.find('div.gdd-transaction-list-item-name').find('a').trigger('click')
|
||||
})
|
||||
|
||||
it('emits set tunneled email', () => {
|
||||
expect(wrapper.emitted('set-tunneled-email')).toEqual([['bibi@bloxberg.de']])
|
||||
it('pushes router to send', () => {
|
||||
expect(routerPushMock).toBeCalledWith({
|
||||
path: '/send',
|
||||
})
|
||||
})
|
||||
|
||||
it('pushes the route with query for email', () => {
|
||||
expect(mocks.$router.push).toBeCalledWith({
|
||||
path: '/send',
|
||||
it('pushes query for gradidoID', () => {
|
||||
expect(routerPushMock).toBeCalledWith({
|
||||
query: {
|
||||
gradidoID: 'gradido-ID',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="name">
|
||||
<div class="gdd-transaction-list-item-name">
|
||||
<div v-if="linkedUser && linkedUser.email">
|
||||
<div v-if="linkedUser && linkedUser.gradidoID">
|
||||
<b-link @click.stop="tunnelEmail" :class="fontColor">
|
||||
{{ itemText }}
|
||||
</b-link>
|
||||
@ -35,8 +35,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
tunnelEmail() {
|
||||
this.$emit('set-tunneled-email', this.linkedUser.email)
|
||||
if (this.$router.history.current.fullPath !== '/send') this.$router.push({ path: '/send' })
|
||||
this.$router.push({ query: { gradidoID: this.linkedUser.gradidoID } })
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
@ -13,7 +13,8 @@ const mocks = {
|
||||
|
||||
const propsData = {
|
||||
amount: '12.45',
|
||||
balance: '31.76099091058521',
|
||||
balance: '31.76',
|
||||
previousBalance: '19.31',
|
||||
balanceDate: '2022-02-28T13:55:47.000Z',
|
||||
decay: {
|
||||
decay: '-0.2038314055482643084',
|
||||
|
||||
@ -18,7 +18,14 @@
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-collapse class="pb-4 pt-lg-3" v-model="visible">
|
||||
<decay-information :typeId="typeId" :decay="decay" :amount="amount" :memo="memo" />
|
||||
<decay-information
|
||||
:typeId="typeId"
|
||||
:decay="decay"
|
||||
:amount="amount"
|
||||
:memo="memo"
|
||||
:balance="balance"
|
||||
:previousBalance="previousBalance"
|
||||
/>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
@ -61,7 +68,11 @@ export default {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
previousBookedBalance: {
|
||||
balance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
previousBalance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
<decay-information-decay
|
||||
:balance="balance"
|
||||
:decay="decay.decay"
|
||||
:previousBookedBalance="previousBookedBalance"
|
||||
:previousBalance="previousBalance"
|
||||
/>
|
||||
</b-collapse>
|
||||
</div>
|
||||
@ -44,9 +44,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
previousBookedBalance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
previousBalance() {
|
||||
return String(Number(this.balance) - Number(this.decay.decay))
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
||||
@ -13,7 +13,8 @@ const mocks = {
|
||||
|
||||
const propsData = {
|
||||
amount: '12.45',
|
||||
balance: '31.76099091058521',
|
||||
balance: '31.76',
|
||||
previousBalance: '19.31',
|
||||
balanceDate: '2022-02-28T13:55:47.000Z',
|
||||
decay: {
|
||||
decay: '-0.2038314055482643084',
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
<div>
|
||||
<name
|
||||
class="font-weight-bold"
|
||||
v-on="$listeners"
|
||||
:amount="amount"
|
||||
:linkedUser="linkedUser"
|
||||
:linkId="linkId"
|
||||
@ -43,7 +42,14 @@
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-collapse class="pb-4 pt-lg-3" v-model="visible">
|
||||
<decay-information :typeId="typeId" :decay="decay" :amount="amount" :memo="memo" />
|
||||
<decay-information
|
||||
:typeId="typeId"
|
||||
:decay="decay"
|
||||
:amount="amount"
|
||||
:memo="memo"
|
||||
:balance="balance"
|
||||
:previousBalance="previousBalance"
|
||||
/>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
@ -85,7 +91,11 @@ export default {
|
||||
typeId: {
|
||||
type: String,
|
||||
},
|
||||
previousBookedBalance: {
|
||||
balance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
previousBalance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
@ -13,7 +13,8 @@ const mocks = {
|
||||
|
||||
const propsData = {
|
||||
amount: '12.45',
|
||||
balance: '31.76099091058521',
|
||||
balance: '31.76',
|
||||
previousBalance: '19.31',
|
||||
balanceDate: '2022-02-28T13:55:47.000Z',
|
||||
decay: {
|
||||
decay: '-0.2038314055482643084',
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
<div>
|
||||
<name
|
||||
class="font-weight-bold"
|
||||
v-on="$listeners"
|
||||
:amount="amount"
|
||||
:linkedUser="linkedUser"
|
||||
:linkId="linkId"
|
||||
@ -42,7 +41,14 @@
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-collapse class="pb-4 pt-lg-3" v-model="visible">
|
||||
<decay-information :typeId="typeId" :decay="decay" :amount="amount" :memo="memo" />
|
||||
<decay-information
|
||||
:typeId="typeId"
|
||||
:decay="decay"
|
||||
:amount="amount"
|
||||
:memo="memo"
|
||||
:balance="balance"
|
||||
:previousBalance="previousBalance"
|
||||
/>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
@ -85,7 +91,11 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
previousBookedBalance: {
|
||||
balance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
previousBalance: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
@ -69,8 +69,8 @@ export const createUser = gql`
|
||||
`
|
||||
|
||||
export const sendCoins = gql`
|
||||
mutation($email: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(email: $email, amount: $amount, memo: $memo)
|
||||
mutation($identifier: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
|
||||
}
|
||||
`
|
||||
|
||||
@ -144,6 +144,7 @@ export const createContributionMessage = gql`
|
||||
export const login = gql`
|
||||
mutation($email: String!, $password: String!, $publisherId: Int) {
|
||||
login(email: $email, password: $password, publisherId: $publisherId) {
|
||||
gradidoID
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
|
||||
@ -33,11 +33,13 @@ export const transactionsQuery = gql`
|
||||
typeId
|
||||
amount
|
||||
balance
|
||||
previousBalance
|
||||
balanceDate
|
||||
memo
|
||||
linkedUser {
|
||||
firstName
|
||||
lastName
|
||||
gradidoID
|
||||
email
|
||||
}
|
||||
decay {
|
||||
@ -268,3 +270,12 @@ export const openCreations = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const user = gql`
|
||||
query($identifier: String!) {
|
||||
user(identifier: $identifier) {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -174,15 +174,6 @@ describe('DashboardLayout', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('set tunneled email', () => {
|
||||
it('updates tunneled email', async () => {
|
||||
await wrapper
|
||||
.findComponent({ ref: 'router-view' })
|
||||
.vm.$emit('set-tunneled-email', 'bibi@bloxberg.de')
|
||||
expect(wrapper.vm.tunneledEmail).toBe('bibi@bloxberg.de')
|
||||
})
|
||||
})
|
||||
|
||||
it('has a component Navbar', () => {
|
||||
expect(wrapper.findComponent({ name: 'Navbar' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
@ -127,7 +127,6 @@
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
@set-tunneled-email="setTunneledEmail"
|
||||
/>
|
||||
</template>
|
||||
<template #community>
|
||||
@ -149,7 +148,6 @@
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
:pending="pending"
|
||||
@update-transactions="updateTransactions"
|
||||
@set-tunneled-email="setTunneledEmail"
|
||||
></router-view>
|
||||
</fade-transition>
|
||||
</div>
|
||||
@ -164,7 +162,6 @@
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
@set-tunneled-email="setTunneledEmail"
|
||||
/>
|
||||
</template>
|
||||
<template #empty />
|
||||
@ -234,18 +231,12 @@ export default {
|
||||
transactionLinkCount: 0,
|
||||
pending: true,
|
||||
visible: false,
|
||||
tunneledEmail: null,
|
||||
hamburger: true,
|
||||
darkMode: false,
|
||||
skeleton: true,
|
||||
totalUsers: null,
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
getTunneledEmail: () => this.tunneledEmail,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.updateTransactions(0)
|
||||
this.getCommunityStatistics()
|
||||
@ -319,9 +310,6 @@ export default {
|
||||
setVisible(bool) {
|
||||
this.visible = bool
|
||||
},
|
||||
setTunneledEmail(email) {
|
||||
this.tunneledEmail = email
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -89,6 +89,8 @@
|
||||
"decay_introduced": "Die Vergänglichkeit wurde eingeführt am:",
|
||||
"decay_since_last_transaction": "Vergänglichkeit seit der letzten Transaktion",
|
||||
"last_transaction": "Letzte Transaktion",
|
||||
"new_balance": "Neuer Kontostand",
|
||||
"old_balance": "Vorheriger Kontostand",
|
||||
"past_time": "Vergangene Zeit",
|
||||
"Starting_block_decay": "Startblock Vergänglichkeit",
|
||||
"total": "Gesamt",
|
||||
@ -100,6 +102,7 @@
|
||||
}
|
||||
},
|
||||
"delete": "Löschen",
|
||||
"download": "Herunterladen",
|
||||
"edit": "bearbeiten",
|
||||
"em-dash": "—",
|
||||
"error": {
|
||||
|
||||
@ -89,6 +89,8 @@
|
||||
"decay_introduced": "Decay was introduced on:",
|
||||
"decay_since_last_transaction": "Decay since the last transaction",
|
||||
"last_transaction": "Last transaction:",
|
||||
"new_balance": "New balance",
|
||||
"old_balance": "Previous balance",
|
||||
"past_time": "Time passed",
|
||||
"Starting_block_decay": "Starting Block Decay",
|
||||
"total": "Total",
|
||||
@ -100,6 +102,7 @@
|
||||
}
|
||||
},
|
||||
"delete": "Delete",
|
||||
"download": "Download",
|
||||
"edit": "edit",
|
||||
"em-dash": "—",
|
||||
"error": {
|
||||
|
||||
@ -88,6 +88,7 @@
|
||||
}
|
||||
},
|
||||
"delete": "Supprimer",
|
||||
"download": "Télécharger",
|
||||
"edit": "modifier",
|
||||
"em-dash": "—",
|
||||
"error": {
|
||||
|
||||
@ -10,6 +10,7 @@ const apolloMutationMock = jest.fn()
|
||||
apolloMutationMock.mockResolvedValue('success')
|
||||
|
||||
const navigatorClipboardMock = jest.fn()
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -38,6 +39,9 @@ describe('Send', () => {
|
||||
$route: {
|
||||
query: {},
|
||||
},
|
||||
$router: {
|
||||
push: routerPushMock,
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
@ -85,8 +89,8 @@ describe('Send', () => {
|
||||
it('shows the transaction formular again', () => {
|
||||
expect(wrapper.findComponent({ name: 'TransactionForm' }).exists()).toBe(true)
|
||||
})
|
||||
// TODO:SKIPED at this point, a check must be made in the components ?
|
||||
it.skip('restores the previous data in the formular', () => {
|
||||
|
||||
it('restores the previous data in the formular', () => {
|
||||
expect(wrapper.find("input[type='email']").vm.$el.value).toBe('user@example.org')
|
||||
expect(wrapper.find("input[type='text']").vm.$el.value).toBe('23.45')
|
||||
expect(wrapper.find('textarea').vm.$el.value).toBe('Make the best of it!')
|
||||
@ -107,10 +111,11 @@ describe('Send', () => {
|
||||
expect.objectContaining({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'user@example.org',
|
||||
identifier: 'user@example.org',
|
||||
amount: 23.45,
|
||||
memo: 'Make the best of it!',
|
||||
selected: SEND_TYPES.send,
|
||||
userName: '',
|
||||
},
|
||||
}),
|
||||
)
|
||||
@ -162,6 +167,67 @@ describe('Send', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('with gradidoID query', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$route.query.gradidoID = 'gradido-ID'
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has no email input field', () => {
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'TransactionForm' }).find('input[type="email"]').exists(),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
describe('submit form', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
const transactionForm = wrapper.findComponent({ name: 'TransactionForm' })
|
||||
await transactionForm.find('input[type="text"]').setValue('34.56')
|
||||
await transactionForm.find('textarea').setValue('Make the best of it!')
|
||||
await transactionForm.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('steps forward in the dialog', () => {
|
||||
expect(wrapper.findComponent({ name: 'TransactionConfirmationSend' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('confirm transaction', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper
|
||||
.findComponent({ name: 'TransactionConfirmationSend' })
|
||||
.find('button.btn-gradido')
|
||||
.trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', async () => {
|
||||
expect(apolloMutationMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
identifier: 'gradido-ID',
|
||||
amount: 34.56,
|
||||
memo: 'Make the best of it!',
|
||||
selected: SEND_TYPES.send,
|
||||
userName: '',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('resets the gradido ID query in route', () => {
|
||||
expect(routerPushMock).toBeCalledWith({
|
||||
query: {
|
||||
gradidoID: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('transaction form link', () => {
|
||||
const now = new Date().toISOString()
|
||||
beforeEach(async () => {
|
||||
|
||||
@ -11,9 +11,7 @@
|
||||
<template #transactionConfirmationSend>
|
||||
<transaction-confirmation-send
|
||||
:balance="balance"
|
||||
:email="transactionData.email"
|
||||
:amount="transactionData.amount"
|
||||
:memo="transactionData.memo"
|
||||
v-bind="transactionData"
|
||||
@send-transaction="sendTransaction"
|
||||
@on-back="onBack"
|
||||
></transaction-confirmation-send>
|
||||
@ -21,7 +19,7 @@
|
||||
<template #transactionConfirmationLink>
|
||||
<transaction-confirmation-link
|
||||
:balance="balance"
|
||||
:email="transactionData.email"
|
||||
:email="transactionData.identifier"
|
||||
:amount="transactionData.amount"
|
||||
:memo="transactionData.memo"
|
||||
:loading="loading"
|
||||
@ -62,7 +60,7 @@ import TransactionResultLink from '@/components/GddSend/TransactionResultLink'
|
||||
import { sendCoins, createTransactionLink } from '@/graphql/mutations.js'
|
||||
|
||||
const EMPTY_TRANSACTION_DATA = {
|
||||
email: '',
|
||||
identifier: '',
|
||||
amount: 0,
|
||||
memo: '',
|
||||
}
|
||||
@ -168,6 +166,7 @@ export default {
|
||||
throw new Error(`undefined transactionData.selected : ${this.transactionData.selected}`)
|
||||
}
|
||||
this.loading = false
|
||||
this.$router.push({ query: { gradidoID: undefined } })
|
||||
},
|
||||
onBack() {
|
||||
this.currentTransactionStep = TRANSACTION_STEPS.transactionForm
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
:showPagination="true"
|
||||
:pageSize="pageSize"
|
||||
@update-transactions="updateTransactions"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user