Merge branch 'master' into 2332-mark-contribution-as-rejected

This commit is contained in:
Hannes Heine 2023-01-19 07:50:22 +01:00 committed by GitHub
commit c84fa68132
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1034 additions and 767 deletions

View File

@ -437,7 +437,7 @@ jobs:
report_name: Coverage Frontend report_name: Coverage Frontend
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 91 min_coverage: 92
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -4,8 +4,60 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.17.0](https://github.com/gradido/gradido/compare/1.16.0...1.17.0)
- fix(frontend): submit contribution text [`#2573`](https://github.com/gradido/gradido/pull/2573)
- fix(backend): admin cannot delete confirmed contribution [`#2571`](https://github.com/gradido/gradido/pull/2571)
- fix(frontend): english locales - horas -> hours [`#2572`](https://github.com/gradido/gradido/pull/2572)
- fix(frontend): mobil divices datepicker add props dropleft [`#2570`](https://github.com/gradido/gradido/pull/2570)
- fix(frontend): pagination [`#2569`](https://github.com/gradido/gradido/pull/2569)
- fix(frontend): add a watch on gdt prop to assure propper loading when mounted [`#2568`](https://github.com/gradido/gradido/pull/2568)
- refactor(frontend): creation step in quarter hour set [`#2566`](https://github.com/gradido/gradido/pull/2566)
- fix(frontend): tunneled email on right side last transactions [`#2561`](https://github.com/gradido/gradido/pull/2561)
- feat(frontend): test transaction page [`#2555`](https://github.com/gradido/gradido/pull/2555)
- refactor(backend): statistics with field resolvers [`#2553`](https://github.com/gradido/gradido/pull/2553)
- fix(frontend): normalized amount transaction if processed again [`#2550`](https://github.com/gradido/gradido/pull/2550)
- fix(backend): semaphore deadlock [`#2551`](https://github.com/gradido/gradido/pull/2551)
- fix(frontend): mobile design [`#2552`](https://github.com/gradido/gradido/pull/2552)
- refactor(frontend): slots for right sidebar and header [`#2548`](https://github.com/gradido/gradido/pull/2548)
- fix(frontend): creation menu highlighted on all submenus [`#2527`](https://github.com/gradido/gradido/pull/2527)
- refactor(frontend): computed hours for open creations [`#2545`](https://github.com/gradido/gradido/pull/2545)
- feat(other): add description for daily backup cronjob [`#2532`](https://github.com/gradido/gradido/pull/2532)
- fix(frontend): editing transaction does not work [`#2543`](https://github.com/gradido/gradido/pull/2543)
- refactor(frontend): remove open creations from store [`#2541`](https://github.com/gradido/gradido/pull/2541)
- feat(other): vscode extensions [`#2524`](https://github.com/gradido/gradido/pull/2524)
- fix(backend): remove jest from dependecies [`#2533`](https://github.com/gradido/gradido/pull/2533)
- fix(frontend): initials without space [`#2546`](https://github.com/gradido/gradido/pull/2546)
- fix(backend): fix backend not confirmable [`#2539`](https://github.com/gradido/gradido/pull/2539)
- fix(frontend): send gdd and send link gdd is running [`#2534`](https://github.com/gradido/gradido/pull/2534)
- fix(other): update browser list [`#2540`](https://github.com/gradido/gradido/pull/2540)
- test(backend): increase backend coverage to 78% [`#2542`](https://github.com/gradido/gradido/pull/2542)
- fix(frontend): pagination gdt [`#2525`](https://github.com/gradido/gradido/pull/2525)
- fix(frontend): leaves are over the user symbol [`#2526`](https://github.com/gradido/gradido/pull/2526)
- fix(frontend): input-email label and placeholder are displayed correctly per language [`#2528`](https://github.com/gradido/gradido/pull/2528)
- feat(backend): add hideAmountGDD & hideAmountGDT to users table. [`#2506`](https://github.com/gradido/gradido/pull/2506)
- fix(frontend): avatar initials always has 2 letters [`#2530`](https://github.com/gradido/gradido/pull/2530)
- refactor(backend): seed contributions as user [`#2460`](https://github.com/gradido/gradido/pull/2460)
- fix(backend): fix logger middleware [`#2503`](https://github.com/gradido/gradido/pull/2503)
- fix(backend): fix email text [`#2523`](https://github.com/gradido/gradido/pull/2523)
- feat(other): new scopes for lint pr [`#2489`](https://github.com/gradido/gradido/pull/2489)
- fix(backend): fix config - some typos [`#2477`](https://github.com/gradido/gradido/pull/2477)
- style(frontend): new Design [`#2297`](https://github.com/gradido/gradido/pull/2297)
- refactor(other): adjust some texts and translations [`#2504`](https://github.com/gradido/gradido/pull/2504)
- test(other): fix tests breaking with the new year [`#2505`](https://github.com/gradido/gradido/pull/2505)
- feat(backend): federation implement exchange of api versions persist in table [`#2427`](https://github.com/gradido/gradido/pull/2427)
- feat(backend): semaphore to lock transaction table [`#2458`](https://github.com/gradido/gradido/pull/2458)
- feat(backend): design html emails and adjust texts [`#2472`](https://github.com/gradido/gradido/pull/2472)
- feat(backend): test semaphore [`#2468`](https://github.com/gradido/gradido/pull/2468)
- fix(admin): reduce triggers of success toast on deleted user form to exactly one [`#2471`](https://github.com/gradido/gradido/pull/2471)
- refactor(other): build nginx docker image in workflow independent of other builds [`#2470`](https://github.com/gradido/gradido/pull/2470)
- feat(backend): setup unit tests for federation [`#2465`](https://github.com/gradido/gradido/pull/2465)
#### [1.16.0](https://github.com/gradido/gradido/compare/1.15.0...1.16.0) #### [1.16.0](https://github.com/gradido/gradido/compare/1.15.0...1.16.0)
> 15 December 2022
- feat(release): version 1.16.0 [`#2467`](https://github.com/gradido/gradido/pull/2467)
- refactor(backend): cleaning user related old password junk [`#2426`](https://github.com/gradido/gradido/pull/2426) - refactor(backend): cleaning user related old password junk [`#2426`](https://github.com/gradido/gradido/pull/2426)
- fix(database): consistent transaction table [`#2453`](https://github.com/gradido/gradido/pull/2453) - fix(database): consistent transaction table [`#2453`](https://github.com/gradido/gradido/pull/2453)
- refactor(backend): dissolve admin resolver [`#2416`](https://github.com/gradido/gradido/pull/2416) - refactor(backend): dissolve admin resolver [`#2416`](https://github.com/gradido/gradido/pull/2416)

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido", "description": "Administraion Interface for Gradido",
"main": "index.js", "main": "index.js",
"author": "Moriz Wahl", "author": "Moriz Wahl",
"version": "1.16.0", "version": "1.17.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "scripts": {

View File

@ -4,12 +4,14 @@ export const communityStatistics = gql`
query { query {
communityStatistics { communityStatistics {
totalUsers totalUsers
activeUsers
deletedUsers deletedUsers
totalGradidoCreated totalGradidoCreated
totalGradidoDecayed totalGradidoDecayed
dynamicStatisticsFields {
activeUsers
totalGradidoAvailable totalGradidoAvailable
totalGradidoUnbookedDecayed totalGradidoUnbookedDecayed
} }
} }
}
` `

View File

@ -17,13 +17,15 @@ const defaultData = () => {
return { return {
communityStatistics: { communityStatistics: {
totalUsers: 3113, totalUsers: 3113,
activeUsers: 1057,
deletedUsers: 35, deletedUsers: 35,
totalGradidoCreated: '4083774.05000000000000000000', totalGradidoCreated: '4083774.05000000000000000000',
totalGradidoDecayed: '-1062639.13634129622923372197', totalGradidoDecayed: '-1062639.13634129622923372197',
dynamicStatisticsFields: {
activeUsers: 1057,
totalGradidoAvailable: '2513565.869444365732411569', totalGradidoAvailable: '2513565.869444365732411569',
totalGradidoUnbookedDecayed: '-500474.6738366222166261272', totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
}, },
},
} }
} }

View File

@ -31,7 +31,9 @@ export default {
return communityStatistics return communityStatistics
}, },
update({ communityStatistics }) { update({ communityStatistics }) {
this.statistics = communityStatistics const totals = { ...communityStatistics.dynamicStatisticsFields }
this.statistics = { ...communityStatistics, ...totals }
delete this.statistics.dynamicStatisticsFields
}, },
error({ message }) { error({ message }) {
this.toastError(message) this.toastError(message)

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-backend", "name": "gradido-backend",
"version": "1.16.0", "version": "1.17.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions", "description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend", "repository": "https://github.com/gradido/gradido/backend",

View File

@ -2,13 +2,25 @@ import { ObjectType, Field } from 'type-graphql'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
@ObjectType() @ObjectType()
export class CommunityStatistics { export class DynamicStatisticsFields {
@Field(() => Number)
totalUsers: number
@Field(() => Number) @Field(() => Number)
activeUsers: number activeUsers: number
@Field(() => Decimal)
totalGradidoAvailable: Decimal
@Field(() => Decimal)
totalGradidoUnbookedDecayed: Decimal
}
@ObjectType()
export class CommunityStatistics {
@Field(() => Number)
allUsers: number
@Field(() => Number)
totalUsers: number
@Field(() => Number) @Field(() => Number)
deletedUsers: number deletedUsers: number
@ -18,9 +30,7 @@ export class CommunityStatistics {
@Field(() => Decimal) @Field(() => Decimal)
totalGradidoDecayed: Decimal totalGradidoDecayed: Decimal
@Field(() => Decimal) // be carefull querying this, takes longer than 2 secs.
totalGradidoAvailable: Decimal @Field(() => DynamicStatisticsFields)
dynamicStatisticsFields: DynamicStatisticsFields
@Field(() => Decimal)
totalGradidoUnbookedDecayed: Decimal
} }

View File

@ -3,6 +3,7 @@
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { stephenHawking } from '@/seeds/users/stephen-hawking' import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { import {
@ -26,7 +27,13 @@ import {
sendContributionConfirmedEmail, sendContributionConfirmedEmail,
// sendContributionRejectedEmail, // sendContributionRejectedEmail,
} from '@/emails/sendEmailVariants' } from '@/emails/sendEmailVariants'
import { cleanDB, resetToken, testEnvironment, contributionDateFormatter } from '@test/helpers' import {
cleanDB,
resetToken,
testEnvironment,
contributionDateFormatter,
resetEntity,
} from '@test/helpers'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { creationFactory } from '@/seeds/factory/creation' import { creationFactory } from '@/seeds/factory/creation'
@ -1818,6 +1825,49 @@ describe('ContributionResolver', () => {
) )
}) })
}) })
describe('creation already confirmed', () => {
it('throws an error', async () => {
await userFactory(testEnv, bobBaumeister)
await query({
query: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
const {
data: { createContribution: confirmedContribution },
} = await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Confirmed Contribution',
creationDate: contributionDateFormatter(new Date()),
},
})
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: confirmContribution,
variables: {
id: confirmedContribution.id ? confirmedContribution.id : -1,
},
})
await expect(
mutate({
mutation: adminDeleteContribution,
variables: {
id: confirmedContribution.id ? confirmedContribution.id : -1,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('A confirmed contribution can not be deleted')],
}),
)
await resetEntity(DbTransaction)
})
})
}) })
describe('confirmContribution', () => { describe('confirmContribution', () => {

View File

@ -513,6 +513,10 @@ export class ContributionResolver {
logger.error(`Contribution not found for given id: ${id}`) logger.error(`Contribution not found for given id: ${id}`)
throw new Error('Contribution not found for given id.') throw new Error('Contribution not found for given id.')
} }
if (contribution.confirmedAt) {
logger.error('A confirmed contribution can not be deleted')
throw new Error('A confirmed contribution can not be deleted')
}
const moderator = getUser(context) const moderator = getUser(context)
if ( if (
contribution.contributionType === ContributionType.USER && contribution.contributionType === ContributionType.USER &&
@ -558,7 +562,6 @@ export class ContributionResolver {
): Promise<boolean> { ): Promise<boolean> {
// acquire lock // acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire() const releaseLock = await TRANSACTIONS_LOCK.acquire()
try { try {
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
const contribution = await DbContribution.findOne(id) const contribution = await DbContribution.findOne(id)
@ -669,7 +672,6 @@ export class ContributionResolver {
} finally { } finally {
releaseLock() releaseLock()
} }
return true return true
} }

View File

@ -1,33 +1,82 @@
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { Resolver, Query, Authorized } from 'type-graphql' import { Resolver, Query, Authorized, FieldResolver } from 'type-graphql'
import { getConnection } from '@dbTools/typeorm' import { getConnection } from '@dbTools/typeorm'
import { Transaction as DbTransaction } from '@entity/Transaction' import { Transaction as DbTransaction } from '@entity/Transaction'
import { User as DbUser } from '@entity/User' import { User as DbUser } from '@entity/User'
import { CommunityStatistics } from '@model/CommunityStatistics' import { CommunityStatistics, DynamicStatisticsFields } from '@model/CommunityStatistics'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ @Resolver((of) => CommunityStatistics)
@Resolver()
export class StatisticsResolver { export class StatisticsResolver {
@Authorized([RIGHTS.COMMUNITY_STATISTICS]) @Authorized([RIGHTS.COMMUNITY_STATISTICS])
@Query(() => CommunityStatistics) @Query(() => CommunityStatistics)
async communityStatistics(): Promise<CommunityStatistics> { async communityStatistics(): Promise<CommunityStatistics> {
const allUsers = await DbUser.count({ withDeleted: true }) return new CommunityStatistics()
const totalUsers = await DbUser.count() }
const deletedUsers = allUsers - totalUsers
@FieldResolver(() => Decimal)
async allUsers(): Promise<number> {
return await DbUser.count({ withDeleted: true })
}
@FieldResolver()
async totalUsers(): Promise<number> {
return await DbUser.count()
}
@FieldResolver()
async deletedUsers(): Promise<number> {
return (await this.allUsers()) - (await this.totalUsers())
}
@FieldResolver()
async totalGradidoCreated(): Promise<Decimal> {
const queryRunner = getConnection().createQueryRunner()
try {
await queryRunner.connect()
const { totalGradidoCreated } = await queryRunner.manager
.createQueryBuilder()
.select('SUM(transaction.amount) AS totalGradidoCreated')
.from(DbTransaction, 'transaction')
.where('transaction.typeId = 1')
.getRawOne()
return totalGradidoCreated
} finally {
await queryRunner.release()
}
}
@FieldResolver()
async totalGradidoDecayed(): Promise<Decimal> {
const queryRunner = getConnection().createQueryRunner()
try {
await queryRunner.connect()
const { totalGradidoDecayed } = await queryRunner.manager
.createQueryBuilder()
.select('SUM(transaction.decay) AS totalGradidoDecayed')
.from(DbTransaction, 'transaction')
.where('transaction.decay IS NOT NULL')
.getRawOne()
return totalGradidoDecayed
} finally {
await queryRunner.release()
}
}
@FieldResolver()
async dynamicStatisticsFields(): Promise<DynamicStatisticsFields> {
let totalGradidoAvailable: Decimal = new Decimal(0) let totalGradidoAvailable: Decimal = new Decimal(0)
let totalGradidoUnbookedDecayed: Decimal = new Decimal(0) let totalGradidoUnbookedDecayed: Decimal = new Decimal(0)
const receivedCallDate = new Date() const receivedCallDate = new Date()
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
try {
await queryRunner.connect() await queryRunner.connect()
const lastUserTransactions = await queryRunner.manager const lastUserTransactions = await queryRunner.manager
@ -52,30 +101,13 @@ export class StatisticsResolver {
} }
}) })
const { totalGradidoCreated } = await queryRunner.manager
.createQueryBuilder()
.select('SUM(transaction.amount) AS totalGradidoCreated')
.from(DbTransaction, 'transaction')
.where('transaction.typeId = 1')
.getRawOne()
const { totalGradidoDecayed } = await queryRunner.manager
.createQueryBuilder()
.select('SUM(transaction.decay) AS totalGradidoDecayed')
.from(DbTransaction, 'transaction')
.where('transaction.decay IS NOT NULL')
.getRawOne()
await queryRunner.release()
return { return {
totalUsers,
activeUsers, activeUsers,
deletedUsers,
totalGradidoCreated,
totalGradidoDecayed,
totalGradidoAvailable, totalGradidoAvailable,
totalGradidoUnbookedDecayed, totalGradidoUnbookedDecayed,
} }
} finally {
await queryRunner.release()
}
} }
} }

View File

@ -4,7 +4,7 @@
import { transactionLinkCode } from './TransactionLinkResolver' import { transactionLinkCode } from './TransactionLinkResolver'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { cleanDB, testEnvironment, resetToken } from '@test/helpers' import { cleanDB, testEnvironment, resetToken, resetEntity } from '@test/helpers'
import { creationFactory } from '@/seeds/factory/creation' import { creationFactory } from '@/seeds/factory/creation'
import { creations } from '@/seeds/creation/index' import { creations } from '@/seeds/creation/index'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
@ -50,7 +50,137 @@ afterAll(async () => {
}) })
describe('TransactionLinkResolver', () => { describe('TransactionLinkResolver', () => {
// TODO: have this test separated into a transactionLink and a contributionLink part (if possible) describe('redeemTransactionLink', () => {
describe('contributionLink', () => {
describe('input not valid', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
it('throws error when link does not exists', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-123456',
},
}),
).resolves.toMatchObject({
errors: [
new GraphQLError(
'Creation from contribution link was not successful. Error: No contribution link found to given code: CL-123456',
),
],
})
})
it('throws error when link is not valid yet', async () => {
const now = new Date()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear() + 1, 0, 1).toISOString(),
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link not valid yet',
),
],
})
await resetEntity(DbContributionLink)
})
it('throws error when contributionLink cycle is invalid', async () => {
const now = new Date()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'INVALID',
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link has unknown cycle',
),
],
})
await resetEntity(DbContributionLink)
})
it('throws error when link is no longer valid', async () => {
const now = new Date()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
validTo: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link is no longer valid',
),
],
})
await resetEntity(DbContributionLink)
})
})
// TODO: have this test separated into a transactionLink and a contributionLink part
describe('redeem daily Contribution Link', () => { describe('redeem daily Contribution Link', () => {
const now = new Date() const now = new Date()
let contributionLink: DbContributionLink | undefined let contributionLink: DbContributionLink | undefined
@ -237,6 +367,7 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
}) })
})
describe('transaction links list', () => { describe('transaction links list', () => {
const variables = { const variables = {
@ -305,7 +436,9 @@ describe('TransactionLinkResolver', () => {
variables.userId = user.id variables.userId = user.id
variables.pageSize = 25 variables.pageSize = 25
// bibi needs GDDs // bibi needs GDDs
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') const bibisCreation = creations.find(
(creation) => creation.email === 'bibi@bloxberg.de',
)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!) await creationFactory(testEnv, bibisCreation!)
// bibis transaktion links // bibis transaktion links
@ -505,9 +638,9 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
}) })
}) })
describe('transactionLinkCode', () => { describe('transactionLinkCode', () => {
const date = new Date() const date = new Date()
it('returns a string of length 24', () => { it('returns a string of length 24', () => {
@ -518,4 +651,5 @@ describe('transactionLinkCode', () => {
const regexp = new RegExp(date.getTime().toString(16) + '$') const regexp = new RegExp(date.getTime().toString(16) + '$')
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp)) expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
}) })
})
}) })

View File

@ -170,6 +170,7 @@ export class TransactionLinkResolver {
if (code.match(/^CL-/)) { if (code.match(/^CL-/)) {
// acquire lock // acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire() const releaseLock = await TRANSACTIONS_LOCK.acquire()
try {
logger.info('redeem contribution link...') logger.info('redeem contribution link...')
const now = new Date() const now = new Date()
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
@ -184,7 +185,7 @@ export class TransactionLinkResolver {
.getOne() .getOne()
if (!contributionLink) { if (!contributionLink) {
logger.error('no contribution link found to given code:', code) logger.error('no contribution link found to given code:', code)
throw new Error('No contribution link found') throw new Error(`No contribution link found to given code: ${code}`)
} }
logger.info('...contribution link found with id', contributionLink.id) logger.info('...contribution link found with id', contributionLink.id)
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) { if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
@ -196,8 +197,11 @@ export class TransactionLinkResolver {
} }
if (contributionLink.validTo) { if (contributionLink.validTo) {
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) { if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
logger.error('contribution link is depricated. Valid to: ', contributionLink.validTo) logger.error(
throw new Error('Contribution link is depricated') 'contribution link is no longer valid. Valid to: ',
contributionLink.validTo,
)
throw new Error('Contribution link is no longer valid')
} }
} }
let alreadyRedeemed: DbContribution | undefined let alreadyRedeemed: DbContribution | undefined
@ -312,6 +316,8 @@ export class TransactionLinkResolver {
throw new Error(`Creation from contribution link was not successful. ${e}`) throw new Error(`Creation from contribution link was not successful. ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
}
} finally {
releaseLock() releaseLock()
} }
return true return true

View File

@ -45,6 +45,9 @@ export const executeTransaction = async (
recipient: dbUser, recipient: dbUser,
transactionLink?: dbTransactionLink | null, transactionLink?: dbTransactionLink | null,
): Promise<boolean> => { ): Promise<boolean> => {
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
try {
logger.info( logger.info(
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`, `executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
) )
@ -64,10 +67,6 @@ export const executeTransaction = async (
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
} }
// acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire()
try {
// validate amount // validate amount
const receivedCallDate = new Date() const receivedCallDate = new Date()
const sendBalance = await calculateBalance( const sendBalance = await calculateBalance(
@ -187,10 +186,10 @@ export const executeTransaction = async (
}) })
} }
logger.info(`finished executeTransaction successfully`) logger.info(`finished executeTransaction successfully`)
return true
} finally { } finally {
releaseLock() releaseLock()
} }
return true
} }
@Resolver() @Resolver()

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-database", "name": "gradido-database",
"version": "1.16.0", "version": "1.17.0",
"description": "Gradido Database Tool to execute database migrations", "description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database", "repository": "https://github.com/gradido/gradido/database",

View File

@ -1,6 +1,6 @@
{ {
"name": "bootstrap-vue-gradido-wallet", "name": "bootstrap-vue-gradido-wallet",
"version": "1.16.0", "version": "1.17.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node run/server.js", "start": "node run/server.js",

View File

@ -23,6 +23,7 @@ describe('ContributionForm', () => {
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$d: jest.fn((d) => d), $d: jest.fn((d) => d),
$n: jest.fn((n) => n),
$store: { $store: {
state: { state: {
creation: ['1000', '1000', '1000'], creation: ['1000', '1000', '1000'],

View File

@ -17,6 +17,7 @@
:label-no-date-selected="$t('contribution.noDateSelected')" :label-no-date-selected="$t('contribution.noDateSelected')"
required required
:disabled="this.form.id !== null" :disabled="this.form.id !== null"
:dropleft="true"
> >
<template #nav-prev-year><span></span></template> <template #nav-prev-year><span></span></template>
<template #nav-next-year><span></span></template> <template #nav-next-year><span></span></template>

View File

@ -9,10 +9,10 @@ describe('InputAmount', () => {
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$n: jest.fn((n) => n),
$i18n: { $i18n: {
locale: jest.fn(() => 'en'), locale: jest.fn(() => 'en'),
}, },
$n: jest.fn((n) => String(n)),
$route: { $route: {
params: {}, params: {},
}, },
@ -46,13 +46,14 @@ describe('InputAmount', () => {
describe('amount normalization', () => { describe('amount normalization', () => {
describe('if invalid', () => { describe('if invalid', () => {
beforeEach(() => { beforeEach(async () => {
await wrapper.setProps({ value: '12m34' })
valid = false valid = false
}) })
it('is not normalized', () => { it('is not normalized', () => {
wrapper.vm.normalizeAmount(valid) wrapper.vm.normalizeAmount(false)
expect(wrapper.vm.amountValue).toBe(0.0) expect(wrapper.vm.currentValue).toBe('12m34')
}) })
}) })
@ -97,13 +98,14 @@ describe('InputAmount', () => {
describe('amount normalization', () => { describe('amount normalization', () => {
describe('if invalid', () => { describe('if invalid', () => {
beforeEach(() => { beforeEach(async () => {
await wrapper.setProps({ value: '12m34' })
valid = false valid = false
}) })
it('is not normalized', () => { it('is not normalized', () => {
wrapper.vm.normalizeAmount(valid) wrapper.vm.normalizeAmount(valid)
expect(wrapper.vm.amountValue).toBe(0.0) expect(wrapper.vm.currentValue).toBe('12m34')
}) })
}) })

View File

@ -20,7 +20,7 @@
trim trim
v-focus="amountFocused" v-focus="amountFocused"
@focus="amountFocused = true" @focus="amountFocused = true"
@blur="normalizeAmount(true)" @blur="normalizeAmount(valid)"
:disabled="disabled" :disabled="disabled"
autocomplete="off" autocomplete="off"
></b-form-input> ></b-form-input>
@ -90,5 +90,8 @@ export default {
this.currentValue = this.$n(this.amountValue, 'ungroupedDecimal') this.currentValue = this.$n(this.amountValue, 'ungroupedDecimal')
}, },
}, },
mounted() {
if (this.value !== '') this.normalizeAmount(true)
},
} }
</script> </script>

View File

@ -15,7 +15,7 @@
:placeholder="placeholder" :placeholder="placeholder"
type="number" type="number"
:state="validated ? valid : false" :state="validated ? valid : false"
step="0.5" step="0.25"
min="0" min="0"
:max="validMaxTime" :max="validMaxTime"
class="bg-248" class="bg-248"

View File

@ -125,14 +125,13 @@
:transactions="transactions" :transactions="transactions"
:transactionCount="transactionCount" :transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount" :transactionLinkCount="transactionLinkCount"
@set-tunneled-email="setTunneledEmail"
> >
<template #transactions> <template #transactions>
<last-transactions <last-transactions
:transactions="transactions" :transactions="transactions"
:transactionCount="transactionCount" :transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount" :transactionLinkCount="transactionLinkCount"
v-on="$listeners" @set-tunneled-email="setTunneledEmail"
/> />
</template> </template>
<template #community> <template #community>
@ -167,14 +166,13 @@
:transactions="transactions" :transactions="transactions"
:transactionCount="transactionCount" :transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount" :transactionLinkCount="transactionLinkCount"
@set-tunneled-email="setTunneledEmail"
> >
<template #transactions> <template #transactions>
<last-transactions <last-transactions
:transactions="transactions" :transactions="transactions"
:transactionCount="transactionCount" :transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount" :transactionLinkCount="transactionLinkCount"
v-on="$listeners" @set-tunneled-email="setTunneledEmail"
/> />
</template> </template>
<template #community> <template #community>

View File

@ -30,7 +30,7 @@
"noOpenContributionLinkText": "Zur Zeit gibt es keine automatischen Schöpfungen.", "noOpenContributionLinkText": "Zur Zeit gibt es keine automatischen Schöpfungen.",
"openContributionLinks": "Öffentliche Beitrags-Linkliste", "openContributionLinks": "Öffentliche Beitrags-Linkliste",
"openContributionLinkText": "Folgende {count} automatische Schöpfungen werden zur Zeit durch die Gemeinschaft „{name}“ bereitgestellt.", "openContributionLinkText": "Folgende {count} automatische Schöpfungen werden zur Zeit durch die Gemeinschaft „{name}“ bereitgestellt.",
"submitContribution": "schreiben" "submitContribution": "Schreiben"
}, },
"communityInfo": "Gemeinschaft Information", "communityInfo": "Gemeinschaft Information",
"contact": "Kontakt", "contact": "Kontakt",

View File

@ -30,7 +30,7 @@
"noOpenContributionLinkText": "Currently there are no automatic creations.", "noOpenContributionLinkText": "Currently there are no automatic creations.",
"openContributionLinks": "Open contribution-link list", "openContributionLinks": "Open contribution-link list",
"openContributionLinkText": "The following {count} automatic creations are currently provided by the \"{name}\" community.", "openContributionLinkText": "The following {count} automatic creations are currently provided by the \"{name}\" community.",
"submitContribution": "writing" "submitContribution": "Contribute"
}, },
"communityInfo": "Community Information", "communityInfo": "Community Information",
"contact": "Contact", "contact": "Contact",
@ -126,7 +126,7 @@
"firstname": "Firstname", "firstname": "Firstname",
"from": "from", "from": "from",
"generate_now": "Generate now", "generate_now": "Generate now",
"hours": "Horas", "hours": "Hours",
"lastname": "Lastname", "lastname": "Lastname",
"memo": "Message", "memo": "Message",
"message": "Message", "message": "Message",

View File

@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Transactions from './Transactions' import Transactions from './Transactions'
import { GdtEntryType } from '@/graphql/enums' import { GdtEntryType } from '@/graphql/enums'
import { listGDTEntriesQuery } from '@/graphql/queries'
import { toastErrorSpy } from '@test/testSetup' import { toastErrorSpy } from '@test/testSetup'
@ -45,8 +46,8 @@ describe('Transactions', () => {
}, },
} }
const Wrapper = () => { const Wrapper = (propsData = {}) => {
return mount(Transactions, { localVue, mocks }) return mount(Transactions, { localVue, mocks, propsData })
} }
describe('mount', () => { describe('mount', () => {
@ -77,26 +78,14 @@ describe('Transactions', () => {
) )
}) })
it.skip('renders the transaction gradido transform table', () => { it('renders the transaction gradido transform table when gdt is true', async () => {
beforeEach(() => { await wrapper.setProps({
wrapper.setData({
gdt: true, gdt: true,
}) })
})
expect(wrapper.findComponent({ name: 'GdtTransactionList' }).exists()).toBeTruthy() expect(wrapper.findComponent({ name: 'GdtTransactionList' }).exists()).toBeTruthy()
}) })
describe.skip('tabs', () => { describe('update gdt with success', () => {
it('shows the GDD transactions by default', () => {
expect(wrapper.findAll('div[role="tabpanel"]').at(0).isVisible()).toBeTruthy()
})
it('does not show the GDT transactions by default', () => {
expect(wrapper.findAll('div[role="tabpanel"]').at(1).isVisible()).toBeFalsy()
})
describe('click on GDT tab', () => {
describe('server returns valid data', () => {
beforeEach(() => { beforeEach(() => {
apolloMock.mockResolvedValue({ apolloMock.mockResolvedValue({
data: { data: {
@ -143,81 +132,57 @@ describe('Transactions', () => {
}, },
}, },
}) })
wrapper.findAll('li[ role="presentation"]').at(1).find('a').trigger('click') wrapper = Wrapper({ gdt: true })
})
it('does not show the GDD transactions', () => {
expect(wrapper.findAll('div[role="tabpanel"]').at(0).isVisible()).toBeFalsy()
})
it('shows the GDT transactions', () => {
expect(wrapper.findAll('div[role="tabpanel"]').at(1).isVisible()).toBeTruthy()
}) })
it('calls the API', () => { it('calls the API', () => {
expect(apolloMock).toBeCalledWith( expect(apolloMock).toBeCalledWith({
expect.objectContaining({ query: listGDTEntriesQuery,
variables: { variables: {
currentPage: 1, currentPage: 1,
pageSize: 25, pageSize: 25,
}, },
}), })
) })
it('does not show the GDD transactions', () => {
expect(wrapper.findAll('div.gdd-transaction-list').exists()).toBeFalsy()
})
it('shows the GDT transactions', () => {
expect(wrapper.findAll('div.gdt-transaction-list').exists()).toBeTruthy()
}) })
it('scrolls to (0, 0) after API call', () => { it('scrolls to (0, 0) after API call', () => {
expect(windowScrollToMock).toBeCalledWith(0, 0) expect(windowScrollToMock).toBeCalledWith(0, 0)
}) })
describe('click on GDD tab', () => { describe('update current page', () => {
beforeEach(() => {
wrapper.findAll('li[ role="presentation"]').at(0).find('a').trigger('click')
})
it('shows the GDD transactions', () => {
expect(wrapper.findAll('div[role="tabpanel"]').at(0).isVisible()).toBeTruthy()
})
it('does not show the GDT', () => {
expect(wrapper.findAll('div[role="tabpanel"]').at(1).isVisible()).toBeFalsy()
})
})
})
describe('server returns error', () => {
beforeEach(() => {
apolloMock.mockRejectedValue({ message: 'Ouch!' })
wrapper.findAll('li[ role="presentation"]').at(1).find('a').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
it('sets transactionGdtCount to -1', () => {
expect(wrapper.vm.transactionGdtCount).toBe(-1)
})
})
})
})
describe.skip('update currentPage', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
wrapper.setData({ wrapper.vm.currentPage = 2
currentPage: 2,
})
}) })
it('calls the API', () => { it('calls the API again', () => {
expect(apolloMock).toBeCalledWith( expect(apolloMock).toBeCalledWith({
expect.objectContaining({ query: listGDTEntriesQuery,
variables: { variables: {
currentPage: 2, currentPage: 2,
pageSize: 25, pageSize: 25,
}, },
}), })
) })
})
})
describe('update gdt with error', () => {
beforeEach(() => {
apolloMock.mockRejectedValue({ message: 'Oh no!' })
wrapper = Wrapper({ gdt: true })
})
it('toasts the error', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
}) })
}) })
}) })

View File

@ -15,6 +15,7 @@
:transactionLinkCount="transactionLinkCount" :transactionLinkCount="transactionLinkCount"
:transactions="transactions" :transactions="transactions"
:showPagination="true" :showPagination="true"
:pageSize="pageSize"
@update-transactions="updateTransactions" @update-transactions="updateTransactions"
v-on="$listeners" v-on="$listeners"
/> />
@ -90,6 +91,11 @@ export default {
this.updateGdt() this.updateGdt()
} }
}, },
gdt() {
if (this.gdt) {
this.updateGdt()
}
},
}, },
} }
</script> </script>

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido", "name": "gradido",
"version": "1.16.0", "version": "1.17.0",
"description": "Gradido", "description": "Gradido",
"main": "index.js", "main": "index.js",
"repository": "git@github.com:gradido/gradido.git", "repository": "git@github.com:gradido/gradido.git",