Fix merge conflict.

This commit is contained in:
elweyn 2022-07-18 12:32:24 +02:00
commit 7a337f027d
27 changed files with 2848 additions and 48 deletions

View File

@ -27,6 +27,9 @@ export enum RIGHTS {
GDT_BALANCE = 'GDT_BALANCE', GDT_BALANCE = 'GDT_BALANCE',
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION', CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
DELETE_CONTRIBUTION = 'DELETE_CONTRIBUTION', DELETE_CONTRIBUTION = 'DELETE_CONTRIBUTION',
LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS',
LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS',
UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION',
// Admin // Admin
SEARCH_USERS = 'SEARCH_USERS', SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE', SET_USER_ROLE = 'SET_USER_ROLE',

View File

@ -25,6 +25,9 @@ export const ROLE_USER = new Role('user', [
RIGHTS.GDT_BALANCE, RIGHTS.GDT_BALANCE,
RIGHTS.CREATE_CONTRIBUTION, RIGHTS.CREATE_CONTRIBUTION,
RIGHTS.DELETE_CONTRIBUTION, RIGHTS.DELETE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTIONS,
RIGHTS.LIST_ALL_CONTRIBUTIONS,
RIGHTS.UPDATE_CONTRIBUTION,
]) ])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -10,7 +10,7 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0040-add_contribution_link_id_to_user', DB_VERSION: '0042-update_transactions_for_blockchain',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info

View File

@ -0,0 +1,60 @@
import { ObjectType, Field, Int } from 'type-graphql'
import Decimal from 'decimal.js-light'
import { Contribution as dbContribution } from '@entity/Contribution'
import { User } from './User'
@ObjectType()
export class Contribution {
constructor(contribution: dbContribution, user: User) {
this.id = contribution.id
this.firstName = user ? user.firstName : null
this.lastName = user ? user.lastName : null
this.amount = contribution.amount
this.memo = contribution.memo
this.createdAt = contribution.createdAt
this.deletedAt = contribution.deletedAt
this.confirmedAt = contribution.confirmedAt
this.confirmedBy = contribution.confirmedBy
}
@Field(() => Number)
id: number
@Field(() => String, { nullable: true })
firstName: string | null
@Field(() => String, { nullable: true })
lastName: string | null
@Field(() => Decimal)
amount: Decimal
@Field(() => String)
memo: string
@Field(() => Date)
createdAt: Date
@Field(() => Date, { nullable: true })
deletedAt: Date | null
@Field(() => Date, { nullable: true })
confirmedAt: Date | null
@Field(() => Number, { nullable: true })
confirmedBy: number | null
}
@ObjectType()
export class ContributionListResult {
constructor(count: number, list: Contribution[]) {
this.linkCount = count
this.linkList = list
}
@Field(() => Int)
linkCount: number
@Field(() => [Contribution])
linkList: Contribution[]
}

View File

@ -1,10 +1,12 @@
import { ObjectType, Field } from 'type-graphql' import { ObjectType, Field } from 'type-graphql'
import { KlickTipp } from './KlickTipp' import { KlickTipp } from './KlickTipp'
import { User as dbUser } from '@entity/User' import { User as dbUser } from '@entity/User'
import Decimal from 'decimal.js-light'
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
@ObjectType() @ObjectType()
export class User { export class User {
constructor(user: dbUser) { constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) {
this.id = user.id this.id = user.id
this.email = user.email this.email = user.email
this.firstName = user.firstName this.firstName = user.firstName
@ -17,6 +19,7 @@ export class User {
this.isAdmin = user.isAdmin this.isAdmin = user.isAdmin
this.klickTipp = null this.klickTipp = null
this.hasElopage = null this.hasElopage = null
this.creation = creation
} }
@Field(() => Number) @Field(() => Number)
@ -64,4 +67,7 @@ export class User {
@Field(() => Boolean, { nullable: true }) @Field(() => Boolean, { nullable: true })
hasElopage: boolean | null hasElopage: boolean | null
@Field(() => [Decimal])
creation: Decimal[]
} }

View File

@ -47,11 +47,11 @@ import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
import CONFIG from '@/config' import CONFIG from '@/config'
import { import {
getCreationIndex,
getUserCreation, getUserCreation,
getUserCreations, getUserCreations,
validateContribution, validateContribution,
isStartEndDateValid, isStartEndDateValid,
updateCreations,
} from './util/creations' } from './util/creations'
import { import {
CONTRIBUTIONLINK_MEMO_MAX_CHARS, CONTRIBUTIONLINK_MEMO_MAX_CHARS,
@ -321,6 +321,10 @@ export class AdminResolver {
throw new Error('user of the pending contribution and send user does not correspond') throw new Error('user of the pending contribution and send user does not correspond')
} }
if (contributionToUpdate.moderatorId === null) {
throw new Error('An admin is not allowed to update a user contribution.')
}
const creationDateObj = new Date(creationDate) const creationDateObj = new Date(creationDate)
let creations = await getUserCreation(user.id) let creations = await getUserCreation(user.id)
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
@ -688,13 +692,3 @@ export class AdminResolver {
return new ContributionLink(dbContributionLink) return new ContributionLink(dbContributionLink)
} }
} }
function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] {
const index = getCreationIndex(contribution.contributionDate.getMonth())
if (index < 0) {
throw new Error('You cannot create GDD for a month older than the last three months.')
}
creations[index] = creations[index].plus(contribution.amount.toString())
return creations
}

View File

@ -2,14 +2,22 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { createContribution } from '@/seeds/graphql/mutations' import {
import { login } from '@/seeds/graphql/queries' adminUpdateContribution,
createContribution,
updateContribution,
} from '@/seeds/graphql/mutations'
import { listAllContributions, listContributions, login } from '@/seeds/graphql/queries'
import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { cleanDB, resetToken, testEnvironment } 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 { creations } from '@/seeds/creation/index'
import { peterLustig } from '@/seeds/users/peter-lustig'
let mutate: any, query: any, con: any let mutate: any, query: any, con: any
let testEnv: any let testEnv: any
let result: any
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment()
@ -111,6 +119,7 @@ describe('ContributionResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
createContribution: { createContribution: {
id: expect.any(Number),
amount: '100', amount: '100',
memo: 'Test env contribution', memo: 'Test env contribution',
}, },
@ -121,4 +130,390 @@ describe('ContributionResolver', () => {
}) })
}) })
}) })
describe('listContributions', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: false,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('filter confirmed is false', () => {
it('returns creations', async () => {
await expect(
query({
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: false,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listContributions: expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
memo: 'Herzlich Willkommen bei Gradido!',
amount: '1000',
}),
expect.objectContaining({
id: expect.any(Number),
memo: 'Test env contribution',
amount: '100',
}),
]),
},
}),
)
})
})
describe('filter confirmed is true', () => {
it('returns only unconfirmed creations', async () => {
await expect(
query({
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: true,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listContributions: expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
memo: 'Test env contribution',
amount: '100',
}),
]),
},
}),
)
})
})
})
})
describe('updateContribution', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: 1,
amount: 100.0,
memo: 'Test Contribution',
creationDate: 'not-valid',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
beforeAll(async () => {
await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bibiBloxberg)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
result = await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('wrong contribution id', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: -1,
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('No contribution found to given id.')],
}),
)
})
})
describe('wrong user tries to update the contribution', () => {
beforeAll(async () => {
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
it('throws an error', async () => {
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: result.data.createContribution.id,
amount: 10.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'user of the pending contribution and send user does not correspond',
),
],
}),
)
})
})
describe('admin tries to update a user contribution', () => {
it('throws an error', async () => {
await expect(
mutate({
mutation: adminUpdateContribution,
variables: {
id: result.data.createContribution.id,
email: 'bibi@bloxberg.de',
amount: 10.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('An admin is not allowed to update a user contribution.')],
}),
)
})
})
describe('update too much so that the limit is exceeded', () => {
beforeAll(async () => {
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
it('throws an error', async () => {
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: result.data.createContribution.id,
amount: 1019.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'The amount (1019 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
),
],
}),
)
})
})
describe('update creation to a date that is older than 3 months', () => {
it('throws an error', async () => {
const date = new Date()
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: result.data.createContribution.id,
amount: 10.0,
memo: 'Test env contribution',
creationDate: date.setMonth(date.getMonth() - 3).toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError('No information for available creations for the given date'),
],
}),
)
})
})
describe('valid input', () => {
it('updates contribution', async () => {
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: result.data.createContribution.id,
amount: 10.0,
memo: 'Test contribution',
creationDate: new Date().toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
updateContribution: {
id: result.data.createContribution.id,
amount: '10',
memo: 'Test contribution',
},
},
}),
)
})
})
})
})
describe('listAllContribution', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: listAllContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: false,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
})
})
it('returns allCreation', async () => {
await expect(
query({
query: listAllContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: false,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listAllContributions: {
linkCount: 2,
linkList: expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
memo: 'Herzlich Willkommen bei Gradido!',
amount: '1000',
}),
expect.objectContaining({
id: expect.any(Number),
memo: 'Test env contribution',
amount: '100',
}),
]),
},
},
}),
)
})
})
})
}) })

View File

@ -1,11 +1,24 @@
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
<<<<<<< HEAD
import { Contribution } from '@entity/Contribution' import { Contribution } from '@entity/Contribution'
import { Arg, Args, Authorized, Ctx, Int, Mutation, Resolver } from 'type-graphql' import { Arg, Args, Authorized, Ctx, Int, Mutation, Resolver } from 'type-graphql'
import ContributionArgs from '../arg/ContributionArgs' import ContributionArgs from '../arg/ContributionArgs'
import { UnconfirmedContribution } from '../model/UnconfirmedContribution' import { UnconfirmedContribution } from '../model/UnconfirmedContribution'
import { validateContribution, getUserCreation } from './util/creations' import { validateContribution, getUserCreation } from './util/creations'
=======
import { Contribution as dbContribution } from '@entity/Contribution'
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
import { FindOperator, IsNull } from '@dbTools/typeorm'
import ContributionArgs from '@arg/ContributionArgs'
import Paginated from '@arg/Paginated'
import { Order } from '@enum/Order'
import { Contribution, ContributionListResult } from '@model/Contribution'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { User } from '@model/User'
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
>>>>>>> master
@Resolver() @Resolver()
export class ContributionResolver { export class ContributionResolver {
@ -21,7 +34,7 @@ export class ContributionResolver {
const creationDateObj = new Date(creationDate) const creationDateObj = new Date(creationDate)
validateContribution(creations, amount, creationDateObj) validateContribution(creations, amount, creationDateObj)
const contribution = Contribution.create() const contribution = dbContribution.create()
contribution.userId = user.id contribution.userId = user.id
contribution.amount = amount contribution.amount = amount
contribution.createdAt = new Date() contribution.createdAt = new Date()
@ -29,10 +42,11 @@ export class ContributionResolver {
contribution.memo = memo contribution.memo = memo
logger.trace('contribution to save', contribution) logger.trace('contribution to save', contribution)
await Contribution.save(contribution) await dbContribution.save(contribution)
return new UnconfirmedContribution(contribution, user, creations) return new UnconfirmedContribution(contribution, user, creations)
} }
<<<<<<< HEAD
@Authorized([RIGHTS.DELETE_CONTRIBUTION]) @Authorized([RIGHTS.DELETE_CONTRIBUTION])
@Mutation(() => Boolean) @Mutation(() => Boolean)
async adminDeleteContribution( async adminDeleteContribution(
@ -49,5 +63,90 @@ export class ContributionResolver {
} }
const res = await contribution.softRemove() const res = await contribution.softRemove()
return !!res return !!res
=======
@Authorized([RIGHTS.LIST_CONTRIBUTIONS])
@Query(() => [Contribution])
async listContributions(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
@Arg('filterConfirmed', () => Boolean)
filterConfirmed: boolean | null,
@Ctx() context: Context,
): Promise<Contribution[]> {
const user = getUser(context)
const where: {
userId: number
confirmedBy?: FindOperator<number> | null
} = { userId: user.id }
if (filterConfirmed) where.confirmedBy = IsNull()
const contributions = await dbContribution.find({
where,
order: {
createdAt: order,
},
withDeleted: true,
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return contributions.map((contribution) => new Contribution(contribution, new User(user)))
}
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTIONS])
@Query(() => ContributionListResult)
async listAllContributions(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
): Promise<ContributionListResult> {
const [dbContributions, count] = await dbContribution.findAndCount({
relations: ['user'],
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return new ContributionListResult(
count,
dbContributions.map(
(contribution) => new Contribution(contribution, new User(contribution.user)),
),
)
}
@Authorized([RIGHTS.UPDATE_CONTRIBUTION])
@Mutation(() => UnconfirmedContribution)
async updateContribution(
@Arg('contributionId', () => Int)
contributionId: number,
@Args() { amount, memo, creationDate }: ContributionArgs,
@Ctx() context: Context,
): Promise<UnconfirmedContribution> {
const user = getUser(context)
const contributionToUpdate = await dbContribution.findOne({
where: { id: contributionId, confirmedAt: IsNull() },
})
if (!contributionToUpdate) {
throw new Error('No contribution found to given id.')
}
if (contributionToUpdate.userId !== user.id) {
throw new Error('user of the pending contribution and send user does not correspond')
}
const creationDateObj = new Date(creationDate)
let creations = await getUserCreation(user.id)
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, contributionToUpdate)
}
// all possible cases not to be true are thrown in this function
validateContribution(creations, amount, creationDateObj)
contributionToUpdate.amount = amount
contributionToUpdate.memo = memo
contributionToUpdate.contributionDate = new Date(creationDate)
dbContribution.save(contributionToUpdate)
return new UnconfirmedContribution(contributionToUpdate, user, creations)
>>>>>>> master
} }
} }

View File

@ -23,6 +23,7 @@ import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegi
import { klicktippSignIn } from '@/apis/KlicktippController' import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys' import { hasElopageBuys } from '@/util/hasElopageBuys'
import { getUserCreation } from './util/creations'
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native') const sodium = require('sodium-native')
@ -224,7 +225,7 @@ export class UserResolver {
logger.info('verifyLogin...') logger.info('verifyLogin...')
// TODO refactor and do not have duplicate code with login(see below) // TODO refactor and do not have duplicate code with login(see below)
const userEntity = getUser(context) const userEntity = getUser(context)
const user = new User(userEntity) const user = new User(userEntity, await getUserCreation(userEntity.id))
// user.pubkey = userEntity.pubKey.toString('hex') // user.pubkey = userEntity.pubKey.toString('hex')
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context) user.hasElopage = await this.hasElopage(context)
@ -274,7 +275,7 @@ export class UserResolver {
logger.addContext('user', dbUser.id) logger.addContext('user', dbUser.id)
logger.debug('login credentials valid...') logger.debug('login credentials valid...')
const user = new User(dbUser) const user = new User(dbUser, await getUserCreation(dbUser.id))
logger.debug('user=' + user) logger.debug('user=' + user)
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId

View File

@ -1,6 +1,7 @@
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { getConnection } from '@dbTools/typeorm' import { getConnection } from '@dbTools/typeorm'
import { Contribution } from '@entity/Contribution'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '../const/const' import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '../const/const'
@ -117,3 +118,13 @@ export const isStartEndDateValid = (
throw new Error(`The value of validFrom must before or equals the validTo!`) throw new Error(`The value of validFrom must before or equals the validTo!`)
} }
} }
export const updateCreations = (creations: Decimal[], contribution: Contribution): Decimal[] => {
const index = getCreationIndex(contribution.contributionDate.getMonth())
if (index < 0) {
throw new Error('You cannot create GDD for a month older than the last three months.')
}
creations[index] = creations[index].plus(contribution.amount.toString())
return creations
}

View File

@ -234,6 +234,22 @@ export const deleteContributionLink = gql`
export const createContribution = gql` export const createContribution = gql`
mutation ($amount: Decimal!, $memo: String!, $creationDate: String!) { mutation ($amount: Decimal!, $memo: String!, $creationDate: String!) {
createContribution(amount: $amount, memo: $memo, creationDate: $creationDate) { createContribution(amount: $amount, memo: $memo, creationDate: $creationDate) {
id
amount
memo
}
}
`
export const updateContribution = gql`
mutation ($contributionId: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
updateContribution(
contributionId: $contributionId
amount: $amount
memo: $memo
creationDate: $creationDate
) {
id
amount amount
memo memo
} }

View File

@ -172,6 +172,43 @@ export const queryTransactionLink = gql`
} }
` `
export const listContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 5
$order: Order
$filterConfirmed: Boolean = false
) {
listContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
filterConfirmed: $filterConfirmed
) {
id
amount
memo
}
}
`
export const listAllContributions = `
query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC) {
listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
linkCount
linkList {
id
firstName
lastName
amount
memo
createdAt
confirmedAt
confirmedBy
}
}
}
`
// from admin interface // from admin interface
export const listUnconfirmedContributions = gql` export const listUnconfirmedContributions = gql`

View File

@ -31,20 +31,24 @@ const filterVariables = (variables: any) => {
const logPlugin = { const logPlugin = {
requestDidStart(requestContext: any) { requestDidStart(requestContext: any) {
const { logger } = requestContext const { logger } = requestContext
const { query, mutation, variables } = requestContext.request const { query, mutation, variables, operationName } = requestContext.request
logger.info(`Request: if (operationName !== 'IntrospectionQuery') {
logger.info(`Request:
${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`) ${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`)
}
return { return {
willSendResponse(requestContext: any) { willSendResponse(requestContext: any) {
if (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`) if (operationName !== 'IntrospectionQuery') {
if (requestContext.response.data) { if (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`)
logger.info('Response Success!') if (requestContext.response.data) {
logger.trace(`Response-Data: logger.info('Response Success!')
logger.trace(`Response-Data:
${JSON.stringify(requestContext.response.data, null, 2)}`) ${JSON.stringify(requestContext.response.data, null, 2)}`)
} }
if (requestContext.response.errors) if (requestContext.response.errors)
logger.error(`Response-Errors: logger.error(`Response-Errors:
${JSON.stringify(requestContext.response.errors, null, 2)}`) ${JSON.stringify(requestContext.response.errors, null, 2)}`)
}
return requestContext return requestContext
}, },
} }

View File

@ -1,6 +1,15 @@
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm' import {
BaseEntity,
Column,
Entity,
PrimaryGeneratedColumn,
DeleteDateColumn,
JoinColumn,
ManyToOne,
} from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { User } from '../User'
@Entity('contributions') @Entity('contributions')
export class Contribution extends BaseEntity { export class Contribution extends BaseEntity {
@ -10,6 +19,10 @@ export class Contribution extends BaseEntity {
@Column({ unsigned: true, nullable: false, name: 'user_id' }) @Column({ unsigned: true, nullable: false, name: 'user_id' })
userId: number userId: number
@ManyToOne(() => User, (user) => user.contributions)
@JoinColumn({ name: 'user_id' })
user: User
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' }) @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
createdAt: Date createdAt: Date

View File

@ -1,4 +1,13 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm' import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
OneToMany,
JoinColumn,
} from 'typeorm'
import { Contribution } from '../Contribution'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) @Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity { export class User extends BaseEntity {
@ -76,4 +85,8 @@ export class User extends BaseEntity {
default: null, default: null,
}) })
passphrase: string passphrase: string
@OneToMany(() => Contribution, (contribution) => contribution.user)
@JoinColumn({ name: 'user_id' })
contributions?: Contribution[]
} }

View File

@ -0,0 +1,130 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
Move forward the creation date of the users by 1 or 2 hours,
for which there are transactions created before their account creation.
Because of a error by importing data from old db format into new, all older transactions balance_date
are 1 or 2 hours off
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
/* generate raw mysql queries
const usersToMove = await queryFn(
`
SELECT u.id, u.created, t.balance_date
FROM \`users\` as u
LEFT JOIN \`transactions\` as t
ON t.user_id = u.id where t.balance_date < u.created
order by id
`
)
let downgradeQueries = ''
for(const id in usersToMove) {
const user = usersToMove[id]
const diff = (user.created - user.balance_date) / 1000
const correcture = diff < 3600 ? 1: 2
const correctedDate = new Date(user.created)
correctedDate.setHours(correctedDate.getHours() - correcture)
//console.log("%d, %s, %s, %s, %d", user.id, user.created, user.balance_date, diff, correcture)
console.log('await queryFn(`UPDATE \\`users\\` SET \\`created\\` = \'%s\' WHERE \\`id\\` = %d`)',
correctedDate.toISOString().slice(0, 19).replace('T', ' '),
user.id
)
downgradeQueries += 'await queryFn(`UPDATE \\`users\\` SET \\`created\\` = \''
downgradeQueries += user.created.toISOString().slice(0, 19).replace('T', ' ')
downgradeQueries += '\' WHERE \\`id\\` = '
downgradeQueries += user.id
downgradeQueries += '`)\n'
}
console.log('downgrade: \n%s', downgradeQueries)
*/
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-01-25 08:01:52' WHERE \`id\` = 179`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-05-26 10:21:57' WHERE \`id\` = 443`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-06-08 17:04:41' WHERE \`id\` = 490`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-06-12 20:07:17' WHERE \`id\` = 508`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-07-17 19:20:36' WHERE \`id\` = 621`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-11-22 16:31:48' WHERE \`id\` = 788`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-12-17 20:09:16' WHERE \`id\` = 825`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-01-26 13:09:35' WHERE \`id\` = 949`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-03-20 16:55:46' WHERE \`id\` = 1057`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 15:59:30' WHERE \`id\` = 1228`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 17:38:47' WHERE \`id\` = 1229`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 17:38:47' WHERE \`id\` = 1229`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 17:38:47' WHERE \`id\` = 1229`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 17:58:15' WHERE \`id\` = 1230`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 17:58:15' WHERE \`id\` = 1230`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-14 14:27:49' WHERE \`id\` = 1231`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-14 14:27:49' WHERE \`id\` = 1231`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-14 14:27:49' WHERE \`id\` = 1231`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-14 14:27:49' WHERE \`id\` = 1231`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-17 22:51:19' WHERE \`id\` = 1239`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-03 07:23:28' WHERE \`id\` = 1273`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-09 06:16:18' WHERE \`id\` = 1287`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-17 11:26:41' WHERE \`id\` = 1298`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-30 15:56:27' WHERE \`id\` = 1315`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-30 15:56:27' WHERE \`id\` = 1315`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-30 15:56:27' WHERE \`id\` = 1315`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-07-08 07:24:57' WHERE \`id\` = 1326`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-07-08 07:24:57' WHERE \`id\` = 1326`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-07-13 12:07:29' WHERE \`id\` = 1342`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-07-16 15:32:48' WHERE \`id\` = 1348`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-09-30 14:06:40' WHERE \`id\` = 1470`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-10-15 14:35:37' WHERE \`id\` = 1490`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-10-16 06:42:00' WHERE \`id\` = 1492`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-10-16 06:42:00' WHERE \`id\` = 1492`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-11-22 09:45:15' WHERE \`id\` = 1576`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-11-23 13:55:37' WHERE \`id\` = 1582`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-11 14:58:12' WHERE \`id\` = 1729`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-11 18:03:10' WHERE \`id\` = 1732`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-19 15:00:38' WHERE \`id\` = 1756`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-19 20:01:58' WHERE \`id\` = 1757`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-21 15:58:48' WHERE \`id\` = 1762`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-01-25 09:01:52' WHERE \`id\` = 179`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-05-26 11:21:57' WHERE \`id\` = 443`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-06-08 19:04:41' WHERE \`id\` = 490`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-06-12 22:07:17' WHERE \`id\` = 508`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-07-17 21:20:36' WHERE \`id\` = 621`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-11-22 17:31:48' WHERE \`id\` = 788`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2020-12-17 21:09:16' WHERE \`id\` = 825`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-01-26 14:09:35' WHERE \`id\` = 949`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-03-20 17:55:46' WHERE \`id\` = 1057`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 17:59:30' WHERE \`id\` = 1228`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 19:38:47' WHERE \`id\` = 1229`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 19:38:47' WHERE \`id\` = 1229`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 19:38:47' WHERE \`id\` = 1229`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 19:58:15' WHERE \`id\` = 1230`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-13 19:58:15' WHERE \`id\` = 1230`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-14 16:27:49' WHERE \`id\` = 1231`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-14 16:27:49' WHERE \`id\` = 1231`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-14 16:27:49' WHERE \`id\` = 1231`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-14 16:27:49' WHERE \`id\` = 1231`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-05-18 00:51:19' WHERE \`id\` = 1239`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-03 09:23:28' WHERE \`id\` = 1273`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-09 08:16:18' WHERE \`id\` = 1287`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-17 13:26:41' WHERE \`id\` = 1298`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-30 17:56:27' WHERE \`id\` = 1315`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-30 17:56:27' WHERE \`id\` = 1315`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-06-30 17:56:27' WHERE \`id\` = 1315`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-07-08 09:24:57' WHERE \`id\` = 1326`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-07-08 09:24:57' WHERE \`id\` = 1326`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-07-13 14:07:29' WHERE \`id\` = 1342`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-07-16 16:32:48' WHERE \`id\` = 1348`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-09-30 16:06:40' WHERE \`id\` = 1470`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-10-15 16:35:37' WHERE \`id\` = 1490`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-10-16 08:42:00' WHERE \`id\` = 1492`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-10-16 08:42:00' WHERE \`id\` = 1492`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-11-22 10:45:15' WHERE \`id\` = 1576`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2021-11-23 14:55:37' WHERE \`id\` = 1582`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-11 15:58:12' WHERE \`id\` = 1729`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-11 19:03:10' WHERE \`id\` = 1732`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-19 16:00:38' WHERE \`id\` = 1756`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-19 21:01:58' WHERE \`id\` = 1757`)
await queryFn(`UPDATE \`users\` SET \`created\` = '2022-01-21 16:58:48' WHERE \`id\` = 1762`)
}

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
{{ $t('auth.left.gratitude') }} {{ $t('auth.left.gratitude') }}
</div> </div>
<div class="position-absolute h2 text-white zindex1000 w-100 text-center mt-9"> <div class="position-absolute h2 text-white zindex1000 w-100 text-center mt-9">
{{ $t('auth.left.newCurrency') }} {{ $t('auth.left.oneGratitude') }}
</div> </div>
<img <img
src="/img/template/Blaetter.png" src="/img/template/Blaetter.png"

View File

@ -62,12 +62,16 @@ describe('SessionLogoutTimeout', () => {
}) })
}) })
describe('token is expired', () => { describe('token is expired for several seconds', () => {
beforeEach(() => { beforeEach(() => {
mocks.$store.state.tokenTime = setTokenTime(-60) mocks.$store.state.tokenTime = setTokenTime(-60)
wrapper = Wrapper() wrapper = Wrapper()
}) })
it('has value for remaining seconds equal 0', () => {
expect(wrapper.tokenExpiresInSeconds === 0)
})
it('emits logout', () => { it('emits logout', () => {
expect(wrapper.emitted('logout')).toBeTruthy() expect(wrapper.emitted('logout')).toBeTruthy()
}) })

View File

@ -65,7 +65,7 @@ export default {
this.$timer.restart('tokenExpires') this.$timer.restart('tokenExpires')
this.$bvModal.show('modalSessionTimeOut') this.$bvModal.show('modalSessionTimeOut')
} }
if (this.tokenExpiresInSeconds <= 0) { if (this.tokenExpiresInSeconds === 0) {
this.$timer.stop('tokenExpires') this.$timer.stop('tokenExpires')
this.$emit('logout') this.$emit('logout')
} }
@ -90,7 +90,10 @@ export default {
}, },
computed: { computed: {
tokenExpiresInSeconds() { tokenExpiresInSeconds() {
return Math.floor((new Date(this.$store.state.tokenTime * 1000).getTime() - this.now) / 1000) const remainingSecs = Math.floor(
(new Date(this.$store.state.tokenTime * 1000).getTime() - this.now) / 1000,
)
return remainingSecs <= 0 ? 0 : remainingSecs
}, },
}, },
beforeDestroy() { beforeDestroy() {

View File

@ -9,15 +9,16 @@ const mockAPIcall = jest.fn()
const navigatorClipboardMock = jest.fn() const navigatorClipboardMock = jest.fn()
const mocks = { const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$d: jest.fn((d) => d), $d: jest.fn((d) => d),
$tc: jest.fn((tc) => tc),
$apollo: { $apollo: {
mutate: mockAPIcall, mutate: mockAPIcall,
}, },
$store: {
state: {
firstName: 'Testy',
},
},
} }
const propsData = { const propsData = {
@ -77,7 +78,7 @@ describe('TransactionLink', () => {
navigator.clipboard = navigatorClipboard navigator.clipboard = navigatorClipboard
}) })
describe('copy with success', () => { describe('copy link with success', () => {
beforeEach(async () => { beforeEach(async () => {
navigatorClipboardMock.mockResolvedValue() navigatorClipboardMock.mockResolvedValue()
await wrapper.find('.test-copy-link .dropdown-item').trigger('click') await wrapper.find('.test-copy-link .dropdown-item').trigger('click')
@ -93,7 +94,26 @@ describe('TransactionLink', () => {
}) })
}) })
describe('copy with error', () => { describe('copy link and text with success', () => {
beforeEach(async () => {
navigatorClipboardMock.mockResolvedValue()
await wrapper.find('.test-copy-text .dropdown-item').trigger('click')
})
it('should call clipboard.writeText', () => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
'http://localhost/redeem/c00000000c000000c0000\n' +
'Testy transaction-link.send_you 75 Gradido.\n' +
'"Katzenauge, Eulenschrei, was verschwunden komm herbei!"\n' +
'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate',
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-and-text-copied')
})
})
describe('copy link with error', () => {
beforeEach(async () => { beforeEach(async () => {
navigatorClipboardMock.mockRejectedValue() navigatorClipboardMock.mockRejectedValue()
await wrapper.find('.test-copy-link .dropdown-item').trigger('click') await wrapper.find('.test-copy-link .dropdown-item').trigger('click')
@ -103,6 +123,17 @@ describe('TransactionLink', () => {
expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied') expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied')
}) })
}) })
describe('copy link and text with error', () => {
beforeEach(async () => {
navigatorClipboardMock.mockRejectedValue()
await wrapper.find('.test-copy-text .dropdown-item').trigger('click')
})
it('toasts an error', () => {
expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied')
})
})
}) })
describe('qr code modal', () => { describe('qr code modal', () => {

View File

@ -22,6 +22,14 @@
<b-icon icon="clipboard"></b-icon> <b-icon icon="clipboard"></b-icon>
{{ $t('gdd_per_link.copy') }} {{ $t('gdd_per_link.copy') }}
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item
v-if="validLink"
class="test-copy-text pt-3"
@click="copyLinkWithText()"
>
<b-icon icon="clipboard-plus"></b-icon>
{{ $t('gdd_per_link.copy-with-text') }}
</b-dropdown-item>
<b-dropdown-item <b-dropdown-item
v-if="validLink" v-if="validLink"
@click="$bvModal.show('modalPopover-' + id)" @click="$bvModal.show('modalPopover-' + id)"
@ -99,6 +107,24 @@ export default {
this.toastError(this.$t('gdd_per_link.not-copied')) this.toastError(this.$t('gdd_per_link.not-copied'))
}) })
}, },
copyLinkWithText() {
navigator.clipboard
.writeText(
`${this.link}
${this.$store.state.firstName} ${this.$t('transaction-link.send_you')} ${this.amount} Gradido.
"${this.memo}"
${this.$t('gdd_per_link.credit-your-gradido')} ${this.$t('gdd_per_link.validUntilDate', {
date: this.$d(new Date(this.validUntil), 'short'),
})}`,
)
.then(() => {
this.toastSuccess(this.$t('gdd_per_link.link-and-text-copied'))
})
.catch(() => {
this.$bvModal.show('modalPopoverCopyError' + this.id)
this.toastError(this.$t('gdd_per_link.not-copied'))
})
},
deleteLink() { deleteLink() {
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.delete-the-link')).then(async (value) => { this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.delete-the-link')).then(async (value) => {
if (value) if (value)

View File

@ -13,6 +13,7 @@ export const login = gql`
hasElopage hasElopage
publisherId publisherId
isAdmin isAdmin
creation
} }
} }
` `
@ -30,6 +31,7 @@ export const verifyLogin = gql`
hasElopage hasElopage
publisherId publisherId
isAdmin isAdmin
creation
} }
} }
` `

View File

@ -12,10 +12,9 @@
"hasAccount": "Du hast schon einen Account?", "hasAccount": "Du hast schon einen Account?",
"hereLogin": "Hier Anmelden", "hereLogin": "Hier Anmelden",
"learnMore": "Erfahre mehr …", "learnMore": "Erfahre mehr …",
"newCurrency": "Die neue Währung",
"oneDignity": "Wir schenken einander und danken mit Gradido.", "oneDignity": "Wir schenken einander und danken mit Gradido.",
"oneDonation": "Du bist ein Geschenk für die Gemeinschaft. 1000 Dank, weil du bei uns bist.", "oneDonation": "Du bist ein Geschenk für die Gemeinschaft. 1000 Dank, weil du bei uns bist.",
"oneGratitude": "Die neue Währung. Für einander, für alle Menschen, für die Natur." "oneGratitude": "Für einander, für alle Menschen, für die Natur."
}, },
"navbar": { "navbar": {
"aboutGradido": "Über Gradido" "aboutGradido": "Über Gradido"
@ -125,7 +124,9 @@
"gdd_per_link": { "gdd_per_link": {
"choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „Jetzt generieren“ wird ein Link erstellt, den du versenden kannst.", "choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „Jetzt generieren“ wird ein Link erstellt, den du versenden kannst.",
"copy": "kopieren", "copy": "kopieren",
"copy-with-text": "Link und Text kopieren",
"created": "Der Link wurde erstellt!", "created": "Der Link wurde erstellt!",
"credit-your-gradido": "Damit die Gradido gutgeschrieben werden können, klicke auf den Link!",
"decay-14-day": "Vergänglichkeit für 14 Tage", "decay-14-day": "Vergänglichkeit für 14 Tage",
"delete-the-link": "Den Link löschen?", "delete-the-link": "Den Link löschen?",
"deleted": "Der Link wurde gelöscht!", "deleted": "Der Link wurde gelöscht!",
@ -133,6 +134,7 @@
"has-account": "Du besitzt bereits ein Gradido Konto?", "has-account": "Du besitzt bereits ein Gradido Konto?",
"header": "Gradidos versenden per Link", "header": "Gradidos versenden per Link",
"isFree": "Gradido ist weltweit kostenfrei.", "isFree": "Gradido ist weltweit kostenfrei.",
"link-and-text-copied": "Der Link und deine Nachricht wurden in die Zwischenablage kopiert. Du kannst ihn jetzt in eine E-Mail oder Nachricht einfügen.",
"link-copied": "Link wurde in die Zwischenablage kopiert. Du kannst ihn jetzt in eine E-Mail oder Nachricht einfügen.", "link-copied": "Link wurde in die Zwischenablage kopiert. Du kannst ihn jetzt in eine E-Mail oder Nachricht einfügen.",
"link-deleted": "Der Link wurde am {date} gelöscht.", "link-deleted": "Der Link wurde am {date} gelöscht.",
"link-expired": "Der Link ist nicht mehr gültig. Die Gültigkeit ist am {date} abgelaufen.", "link-expired": "Der Link ist nicht mehr gültig. Die Gültigkeit ist am {date} abgelaufen.",
@ -149,7 +151,8 @@
"redeemed-title": "eingelöst", "redeemed-title": "eingelöst",
"to-login": "Log dich ein", "to-login": "Log dich ein",
"to-register": "Registriere ein neues Konto.", "to-register": "Registriere ein neues Konto.",
"validUntil": "Gültig bis" "validUntil": "Gültig bis",
"validUntilDate": "Der Link ist bis zum {date} gültig."
}, },
"gdt": { "gdt": {
"calculation": "Berechnung der Gradido Transform", "calculation": "Berechnung der Gradido Transform",

View File

@ -12,10 +12,9 @@
"hasAccount": "You already have an account?", "hasAccount": "You already have an account?",
"hereLogin": "Log in here", "hereLogin": "Log in here",
"learnMore": "Learn more …", "learnMore": "Learn more …",
"newCurrency": "The new currency",
"oneDignity": "We gift to each other and give thanks with Gradido.", "oneDignity": "We gift to each other and give thanks with Gradido.",
"oneDonation": "You are a gift for the community. 1000 thanks because you are with us.", "oneDonation": "You are a gift for the community. 1000 thanks because you are with us.",
"oneGratitude": "The new currency. For each other, for all people, for nature." "oneGratitude": "For each other, for all people, for nature."
}, },
"navbar": { "navbar": {
"aboutGradido": "About Gradido" "aboutGradido": "About Gradido"
@ -125,7 +124,9 @@
"gdd_per_link": { "gdd_per_link": {
"choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.", "choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.",
"copy": "copy", "copy": "copy",
"copy-with-text": "Copy link and text",
"created": "Link was created!", "created": "Link was created!",
"credit-your-gradido": "For the Gradido to be credited, click on the link!",
"decay-14-day": "Decay for 14 days", "decay-14-day": "Decay for 14 days",
"delete-the-link": "Delete the link?", "delete-the-link": "Delete the link?",
"deleted": "The link was deleted!", "deleted": "The link was deleted!",
@ -133,6 +134,7 @@
"has-account": "You already have a Gradido account?", "has-account": "You already have a Gradido account?",
"header": "Send Gradidos via link", "header": "Send Gradidos via link",
"isFree": "Gradido is free of charge worldwide.", "isFree": "Gradido is free of charge worldwide.",
"link-and-text-copied": "The link and your message have been copied to the clipboard. You can now include it in an email or message.",
"link-copied": "Link has been copied to the clipboard. You can now paste it into an email or message.", "link-copied": "Link has been copied to the clipboard. You can now paste it into an email or message.",
"link-deleted": "The link was deleted on {date}.", "link-deleted": "The link was deleted on {date}.",
"link-expired": "The link is no longer valid. The validity expired on {date}.", "link-expired": "The link is no longer valid. The validity expired on {date}.",
@ -149,7 +151,8 @@
"redeemed-title": "redeemed", "redeemed-title": "redeemed",
"to-login": "Log in", "to-login": "Log in",
"to-register": "Register a new account.", "to-register": "Register a new account.",
"validUntil": "Valid until" "validUntil": "Valid until",
"validUntilDate": "The link is valid until {date}."
}, },
"gdt": { "gdt": {
"calculation": "Calculation of Gradido Transform", "calculation": "Calculation of Gradido Transform",

View File

@ -47,6 +47,9 @@ export const mutations = {
hasElopage: (state, hasElopage) => { hasElopage: (state, hasElopage) => {
state.hasElopage = hasElopage state.hasElopage = hasElopage
}, },
creation: (state, creation) => {
state.creation = creation
},
} }
export const actions = { export const actions = {
@ -60,6 +63,7 @@ export const actions = {
commit('hasElopage', data.hasElopage) commit('hasElopage', data.hasElopage)
commit('publisherId', data.publisherId) commit('publisherId', data.publisherId)
commit('isAdmin', data.isAdmin) commit('isAdmin', data.isAdmin)
commit('creation', data.creation)
}, },
logout: ({ commit, state }) => { logout: ({ commit, state }) => {
commit('token', null) commit('token', null)
@ -71,6 +75,7 @@ export const actions = {
commit('hasElopage', false) commit('hasElopage', false)
commit('publisherId', null) commit('publisherId', null)
commit('isAdmin', false) commit('isAdmin', false)
commit('creation', null)
localStorage.clear() localStorage.clear()
}, },
} }
@ -96,6 +101,7 @@ try {
newsletterState: null, newsletterState: null,
hasElopage: false, hasElopage: false,
publisherId: null, publisherId: null,
creation: null,
}, },
getters: {}, getters: {},
// Syncronous mutation of the state // Syncronous mutation of the state

View File

@ -30,6 +30,7 @@ const {
publisherId, publisherId,
isAdmin, isAdmin,
hasElopage, hasElopage,
creation,
} = mutations } = mutations
const { login, logout } = actions const { login, logout } = actions
@ -139,6 +140,14 @@ describe('Vuex store', () => {
expect(state.hasElopage).toBeTruthy() expect(state.hasElopage).toBeTruthy()
}) })
}) })
describe('creation', () => {
it('sets the state of creation', () => {
const state = { creation: null }
creation(state, true)
expect(state.creation).toEqual(true)
})
})
}) })
describe('actions', () => { describe('actions', () => {
@ -156,11 +165,12 @@ describe('Vuex store', () => {
hasElopage: false, hasElopage: false,
publisherId: 1234, publisherId: 1234,
isAdmin: true, isAdmin: true,
creation: ['1000', '1000', '1000'],
} }
it('calls nine commits', () => { it('calls nine commits', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(8) expect(commit).toHaveBeenCalledTimes(9)
}) })
it('commits email', () => { it('commits email', () => {
@ -202,6 +212,11 @@ describe('Vuex store', () => {
login({ commit, state }, commitedData) login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', true) expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', true)
}) })
it('commits creation', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(9, 'creation', ['1000', '1000', '1000'])
})
}) })
describe('logout', () => { describe('logout', () => {
@ -210,7 +225,7 @@ describe('Vuex store', () => {
it('calls nine commits', () => { it('calls nine commits', () => {
logout({ commit, state }) logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(8) expect(commit).toHaveBeenCalledTimes(9)
}) })
it('commits token', () => { it('commits token', () => {
@ -253,6 +268,11 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', false) expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', false)
}) })
it('commits creation', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(9, 'creation', null)
})
// how to get this working? // how to get this working?
it.skip('calls localStorage.clear()', () => { it.skip('calls localStorage.clear()', () => {
const clearStorageMock = jest.fn() const clearStorageMock = jest.fn()