mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' of github.com:gradido/gradido into 1881-set-role-in-admin-interface
This commit is contained in:
commit
0b464591a1
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -528,7 +528,7 @@ jobs:
|
|||||||
report_name: Coverage Backend
|
report_name: Coverage Backend
|
||||||
type: lcov
|
type: lcov
|
||||||
result_path: ./backend/coverage/lcov.info
|
result_path: ./backend/coverage/lcov.info
|
||||||
min_coverage: 70
|
min_coverage: 68
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|||||||
18
CHANGELOG.md
18
CHANGELOG.md
@ -4,8 +4,26 @@ 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.10.0](https://github.com/gradido/gradido/compare/1.9.0...1.10.0)
|
||||||
|
|
||||||
|
- frontend redeem contribution link [`#1988`](https://github.com/gradido/gradido/pull/1988)
|
||||||
|
- change new start picture [`#1990`](https://github.com/gradido/gradido/pull/1990)
|
||||||
|
- feat: Redeem Contribution Link [`#1987`](https://github.com/gradido/gradido/pull/1987)
|
||||||
|
- fix: Max Amount on Slider for Edit Contribution [`#1986`](https://github.com/gradido/gradido/pull/1986)
|
||||||
|
- CRUD contribution link admin interface [`#1981`](https://github.com/gradido/gradido/pull/1981)
|
||||||
|
- fix: `.env` log level for apollo and backend category [`#1967`](https://github.com/gradido/gradido/pull/1967)
|
||||||
|
- refactor: Admin Pending Creations Table to Contributions Table [`#1949`](https://github.com/gradido/gradido/pull/1949)
|
||||||
|
- devops: Update Browser List for Unit Tests as Recomended [`#1984`](https://github.com/gradido/gradido/pull/1984)
|
||||||
|
- feat: CRUD for Contribution Links in Admin Resolver [`#1979`](https://github.com/gradido/gradido/pull/1979)
|
||||||
|
- 1920 feature create contribution link table [`#1957`](https://github.com/gradido/gradido/pull/1957)
|
||||||
|
- refactor: 🍰 Delete `user_setting` Table From DB [`#1960`](https://github.com/gradido/gradido/pull/1960)
|
||||||
|
- locales link german, english navbar [`#1969`](https://github.com/gradido/gradido/pull/1969)
|
||||||
|
|
||||||
#### [1.9.0](https://github.com/gradido/gradido/compare/1.8.3...1.9.0)
|
#### [1.9.0](https://github.com/gradido/gradido/compare/1.8.3...1.9.0)
|
||||||
|
|
||||||
|
> 2 June 2022
|
||||||
|
|
||||||
|
- devops: Release Version 1.9.0 [`#1968`](https://github.com/gradido/gradido/pull/1968)
|
||||||
- refactor: 🍰 Refactor To `filters` Object And Rename Filters Properties [`#1914`](https://github.com/gradido/gradido/pull/1914)
|
- refactor: 🍰 Refactor To `filters` Object And Rename Filters Properties [`#1914`](https://github.com/gradido/gradido/pull/1914)
|
||||||
- refactor register button position [`#1964`](https://github.com/gradido/gradido/pull/1964)
|
- refactor register button position [`#1964`](https://github.com/gradido/gradido/pull/1964)
|
||||||
- fixed redeem link is mobile start false [`#1958`](https://github.com/gradido/gradido/pull/1958)
|
- fixed redeem link is mobile start false [`#1958`](https://github.com/gradido/gradido/pull/1958)
|
||||||
|
|||||||
@ -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.9.0",
|
"version": "1.10.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -181,6 +181,7 @@ export default {
|
|||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.link = result.data.createContributionLink.link
|
this.link = result.data.createContributionLink.link
|
||||||
this.toastSuccess(this.link)
|
this.toastSuccess(this.link)
|
||||||
|
this.onReset()
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.toastError(error.message)
|
this.toastError(error.message)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-backend",
|
"name": "gradido-backend",
|
||||||
"version": "1.9.0",
|
"version": "1.10.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",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0039-contributions_table',
|
DB_VERSION: '0040-add_contribution_link_id_to_user',
|
||||||
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
|
||||||
|
|||||||
@ -650,7 +650,7 @@ interface CreationMap {
|
|||||||
creations: Decimal[]
|
creations: Decimal[]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserCreation(id: number, includePending = true): Promise<Decimal[]> {
|
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||||
logger.trace('getUserCreation', id, includePending)
|
logger.trace('getUserCreation', id, includePending)
|
||||||
const creations = await getUserCreations([id], includePending)
|
const creations = await getUserCreations([id], includePending)
|
||||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||||
@ -713,7 +713,11 @@ function updateCreations(creations: Decimal[], contribution: Contribution): Deci
|
|||||||
return creations
|
return creations
|
||||||
}
|
}
|
||||||
|
|
||||||
function isContributionValid(creations: Decimal[], amount: Decimal, creationDate: Date) {
|
export const isContributionValid = (
|
||||||
|
creations: Decimal[],
|
||||||
|
amount: Decimal,
|
||||||
|
creationDate: Date,
|
||||||
|
): boolean => {
|
||||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
logger.trace('isContributionValid', creations, amount, creationDate)
|
||||||
const index = getCreationIndex(creationDate.getMonth())
|
const index = getCreationIndex(creationDate.getMonth())
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,21 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import { Context, getUser } from '@/server/context'
|
import { Context, getUser } from '@/server/context'
|
||||||
import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql'
|
import { getConnection } from '@dbTools/typeorm'
|
||||||
|
import {
|
||||||
|
Resolver,
|
||||||
|
Args,
|
||||||
|
Arg,
|
||||||
|
Authorized,
|
||||||
|
Ctx,
|
||||||
|
Mutation,
|
||||||
|
Query,
|
||||||
|
Int,
|
||||||
|
createUnionType,
|
||||||
|
} from 'type-graphql'
|
||||||
import { TransactionLink } from '@model/TransactionLink'
|
import { TransactionLink } from '@model/TransactionLink'
|
||||||
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
|
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import TransactionLinkArgs from '@arg/TransactionLinkArgs'
|
import TransactionLinkArgs from '@arg/TransactionLinkArgs'
|
||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
@ -12,6 +26,17 @@ import { User } from '@model/User'
|
|||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import { executeTransaction } from './TransactionResolver'
|
import { executeTransaction } from './TransactionResolver'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
|
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||||
|
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||||
|
import { getUserCreation, isContributionValid } from './AdminResolver'
|
||||||
|
import { Decay } from '@model/Decay'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||||
|
|
||||||
|
const QueryLinkResult = createUnionType({
|
||||||
|
name: 'QueryLinkResult', // the name of the GraphQL union
|
||||||
|
types: () => [TransactionLink, ContributionLink] as const, // function that returns tuple of object types classes
|
||||||
|
})
|
||||||
|
|
||||||
// TODO: do not export, test it inside the resolver
|
// TODO: do not export, test it inside the resolver
|
||||||
export const transactionLinkCode = (date: Date): string => {
|
export const transactionLinkCode = (date: Date): string => {
|
||||||
@ -95,8 +120,15 @@ export class TransactionLinkResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
|
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
|
||||||
@Query(() => TransactionLink)
|
@Query(() => QueryLinkResult)
|
||||||
async queryTransactionLink(@Arg('code') code: string): Promise<TransactionLink> {
|
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
|
||||||
|
if (code.match(/^CL-/)) {
|
||||||
|
const contributionLink = await DbContributionLink.findOneOrFail(
|
||||||
|
{ code: code.replace('CL-', '') },
|
||||||
|
{ withDeleted: true },
|
||||||
|
)
|
||||||
|
return new ContributionLink(contributionLink)
|
||||||
|
} else {
|
||||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
||||||
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
const user = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||||
let redeemedBy: User | null = null
|
let redeemedBy: User | null = null
|
||||||
@ -105,6 +137,7 @@ export class TransactionLinkResolver {
|
|||||||
}
|
}
|
||||||
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
|
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS])
|
||||||
@Query(() => [TransactionLink])
|
@Query(() => [TransactionLink])
|
||||||
@ -137,11 +170,122 @@ export class TransactionLinkResolver {
|
|||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
if (code.match(/^CL-/)) {
|
||||||
|
logger.info('redeem contribution link...')
|
||||||
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
|
await queryRunner.connect()
|
||||||
|
await queryRunner.startTransaction('SERIALIZABLE')
|
||||||
|
try {
|
||||||
|
const contributionLink = await queryRunner.manager
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('contributionLink')
|
||||||
|
.from(DbContributionLink, 'contributionLink')
|
||||||
|
.where('contributionLink.code = :code', { code: code.replace('CL-', '') })
|
||||||
|
.getOne()
|
||||||
|
if (!contributionLink) {
|
||||||
|
logger.error('no contribution link found to given code:', code)
|
||||||
|
throw new Error('No contribution link found')
|
||||||
|
}
|
||||||
|
logger.info('...contribution link found with id', contributionLink.id)
|
||||||
|
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
|
||||||
|
logger.error(
|
||||||
|
'contribution link is not valid yet. Valid from: ',
|
||||||
|
contributionLink.validFrom,
|
||||||
|
)
|
||||||
|
throw new Error('Contribution link not valid yet')
|
||||||
|
}
|
||||||
|
if (contributionLink.validTo) {
|
||||||
|
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
|
||||||
|
logger.error('contribution link is depricated. Valid to: ', contributionLink.validTo)
|
||||||
|
throw new Error('Contribution link is depricated')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contributionLink.cycle !== 'ONCE') {
|
||||||
|
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
||||||
|
throw new Error('Contribution link has unknown cycle')
|
||||||
|
}
|
||||||
|
// Test ONCE rule
|
||||||
|
const alreadyRedeemed = await queryRunner.manager
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('contribution')
|
||||||
|
.from(DbContribution, 'contribution')
|
||||||
|
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
||||||
|
linkId: contributionLink.id,
|
||||||
|
id: user.id,
|
||||||
|
})
|
||||||
|
.getOne()
|
||||||
|
if (alreadyRedeemed) {
|
||||||
|
logger.error('contribution link with rule ONCE already redeemed by user with id', user.id)
|
||||||
|
throw new Error('Contribution link already redeemed')
|
||||||
|
}
|
||||||
|
|
||||||
|
const creations = await getUserCreation(user.id, false)
|
||||||
|
logger.info('open creations', creations)
|
||||||
|
if (!isContributionValid(creations, contributionLink.amount, now)) {
|
||||||
|
logger.error(
|
||||||
|
'Amount of Contribution link exceeds available amount for this month',
|
||||||
|
contributionLink.amount,
|
||||||
|
)
|
||||||
|
throw new Error('Amount of Contribution link exceeds available amount')
|
||||||
|
}
|
||||||
|
const contribution = new DbContribution()
|
||||||
|
contribution.userId = user.id
|
||||||
|
contribution.createdAt = now
|
||||||
|
contribution.contributionDate = now
|
||||||
|
contribution.memo = contributionLink.memo
|
||||||
|
contribution.amount = contributionLink.amount
|
||||||
|
contribution.contributionLinkId = contributionLink.id
|
||||||
|
await queryRunner.manager.insert(DbContribution, contribution)
|
||||||
|
|
||||||
|
const lastTransaction = await queryRunner.manager
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('transaction')
|
||||||
|
.from(DbTransaction, 'transaction')
|
||||||
|
.where('transaction.userId = :id', { id: user.id })
|
||||||
|
.orderBy('transaction.balanceDate', 'DESC')
|
||||||
|
.getOne()
|
||||||
|
let newBalance = new Decimal(0)
|
||||||
|
|
||||||
|
let decay: Decay | null = null
|
||||||
|
if (lastTransaction) {
|
||||||
|
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now)
|
||||||
|
newBalance = decay.balance
|
||||||
|
}
|
||||||
|
newBalance = newBalance.add(contributionLink.amount.toString())
|
||||||
|
|
||||||
|
const transaction = new DbTransaction()
|
||||||
|
transaction.typeId = TransactionTypeId.CREATION
|
||||||
|
transaction.memo = contribution.memo
|
||||||
|
transaction.userId = contribution.userId
|
||||||
|
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||||
|
transaction.amount = contribution.amount
|
||||||
|
transaction.creationDate = contribution.contributionDate
|
||||||
|
transaction.balance = newBalance
|
||||||
|
transaction.balanceDate = now
|
||||||
|
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||||
|
transaction.decayStart = decay ? decay.start : null
|
||||||
|
await queryRunner.manager.insert(DbTransaction, transaction)
|
||||||
|
|
||||||
|
contribution.confirmedAt = now
|
||||||
|
contribution.transactionId = transaction.id
|
||||||
|
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction()
|
||||||
|
logger.info('creation from contribution link commited successfuly.')
|
||||||
|
} catch (e) {
|
||||||
|
await queryRunner.rollbackTransaction()
|
||||||
|
logger.error(`Creation from contribution link was not successful: ${e}`)
|
||||||
|
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
if (user.id === linkedUser.id) {
|
if (user.id === linkedUser.id) {
|
||||||
throw new Error('Cannot redeem own transaction link.')
|
throw new Error('Cannot redeem own transaction link.')
|
||||||
}
|
}
|
||||||
@ -164,4 +308,5 @@ export class TransactionLinkResolver {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,10 @@ import CONFIG from '@/config'
|
|||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { printTimeDuration, activationLink } from './UserResolver'
|
import { printTimeDuration, activationLink } from './UserResolver'
|
||||||
|
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||||
|
// import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||||
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
|
// import { TransactionLink } from '@entity/TransactionLink'
|
||||||
|
|
||||||
import { logger } from '@test/testSetup'
|
import { logger } from '@test/testSetup'
|
||||||
|
|
||||||
@ -69,6 +73,7 @@ describe('UserResolver', () => {
|
|||||||
|
|
||||||
let result: any
|
let result: any
|
||||||
let emailOptIn: string
|
let emailOptIn: string
|
||||||
|
let user: User[]
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
@ -86,7 +91,6 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('valid input data', () => {
|
describe('valid input data', () => {
|
||||||
let user: User[]
|
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
let loginEmailOptIn: LoginEmailOptIn[]
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
user = await User.find()
|
user = await User.find()
|
||||||
@ -114,6 +118,7 @@ describe('UserResolver', () => {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
publisherId: 1234,
|
publisherId: 1234,
|
||||||
referrerId: null,
|
referrerId: null,
|
||||||
|
contributionLinkId: null,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@ -195,6 +200,72 @@ describe('UserResolver', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('redeem codes', () => {
|
||||||
|
describe('contribution link', () => {
|
||||||
|
let link: ContributionLink
|
||||||
|
beforeAll(async () => {
|
||||||
|
// activate account of admin Peter Lustig
|
||||||
|
await mutate({
|
||||||
|
mutation: setPassword,
|
||||||
|
variables: { code: emailOptIn, password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
// make Peter Lustig Admin
|
||||||
|
const peter = await User.findOneOrFail({ id: user[0].id })
|
||||||
|
peter.isAdmin = new Date()
|
||||||
|
await peter.save()
|
||||||
|
// factory logs in as Peter Lustig
|
||||||
|
link = await contributionLinkFactory(testEnv, {
|
||||||
|
name: 'Dokumenta 2022',
|
||||||
|
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
||||||
|
amount: 200,
|
||||||
|
validFrom: new Date(2022, 5, 18),
|
||||||
|
validTo: new Date(2022, 8, 25),
|
||||||
|
})
|
||||||
|
resetToken()
|
||||||
|
await mutate({
|
||||||
|
mutation: createUser,
|
||||||
|
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the contribution link id', async () => {
|
||||||
|
await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
contributionLinkId: link.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/* A transaction link requires GDD on account
|
||||||
|
describe('transaction link', () => {
|
||||||
|
let code: string
|
||||||
|
beforeAll(async () => {
|
||||||
|
// factory logs in as Peter Lustig
|
||||||
|
await transactionLinkFactory(testEnv, {
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
amount: 19.99,
|
||||||
|
memo: `Kein Trick, keine Zauberrei,
|
||||||
|
bei Gradidio sei dabei!`,
|
||||||
|
})
|
||||||
|
const transactionLink = await TransactionLink.findOneOrFail()
|
||||||
|
resetToken()
|
||||||
|
await mutate({
|
||||||
|
mutation: createUser,
|
||||||
|
variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the referrer id to Peter Lustigs id', async () => {
|
||||||
|
await expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual(expect.objectContaining({
|
||||||
|
referrerId: user[0].id,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
*/
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('setPassword', () => {
|
describe('setPassword', () => {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import CONFIG from '@/config'
|
|||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
|
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||||
import { encode } from '@/auth/JWT'
|
import { encode } from '@/auth/JWT'
|
||||||
import CreateUserArgs from '@arg/CreateUserArgs'
|
import CreateUserArgs from '@arg/CreateUserArgs'
|
||||||
import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
||||||
@ -349,12 +350,22 @@ export class UserResolver {
|
|||||||
dbUser.passphrase = passphrase.join(' ')
|
dbUser.passphrase = passphrase.join(' ')
|
||||||
logger.debug('new dbUser=' + dbUser)
|
logger.debug('new dbUser=' + dbUser)
|
||||||
if (redeemCode) {
|
if (redeemCode) {
|
||||||
|
if (redeemCode.match(/^CL-/)) {
|
||||||
|
const contributionLink = await dbContributionLink.findOne({
|
||||||
|
code: redeemCode.replace('CL-', ''),
|
||||||
|
})
|
||||||
|
logger.info('redeemCode found contributionLink=' + contributionLink)
|
||||||
|
if (contributionLink) {
|
||||||
|
dbUser.contributionLinkId = contributionLink.id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
||||||
logger.info('redeemCode found transactionLink=' + transactionLink)
|
logger.info('redeemCode found transactionLink=' + transactionLink)
|
||||||
if (transactionLink) {
|
if (transactionLink) {
|
||||||
dbUser.referrerId = transactionLink.userId
|
dbUser.referrerId = transactionLink.userId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// TODO this field has no null allowed unlike the loginServer table
|
// TODO this field has no null allowed unlike the loginServer table
|
||||||
// dbUser.pubKey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000...
|
// dbUser.pubKey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000...
|
||||||
// dbUser.pubkey = keyPair[0]
|
// dbUser.pubkey = keyPair[0]
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
import { createContributionLink } from '@/seeds/graphql/mutations'
|
import { createContributionLink } from '@/seeds/graphql/mutations'
|
||||||
import { login } from '@/seeds/graphql/queries'
|
import { login } from '@/seeds/graphql/queries'
|
||||||
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
|
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
|
||||||
|
|
||||||
export const contributionLinkFactory = async (
|
export const contributionLinkFactory = async (
|
||||||
client: ApolloServerTestClient,
|
client: ApolloServerTestClient,
|
||||||
contributionLink: ContributionLinkInterface,
|
contributionLink: ContributionLinkInterface,
|
||||||
): Promise<void> => {
|
): Promise<ContributionLink> => {
|
||||||
const { mutate, query } = client
|
const { mutate, query } = client
|
||||||
|
|
||||||
// login as admin
|
// login as admin
|
||||||
@ -23,5 +24,6 @@ export const contributionLinkFactory = async (
|
|||||||
validTo: contributionLink.validTo ? contributionLink.validTo.toISOString() : undefined,
|
validTo: contributionLink.validTo ? contributionLink.validTo.toISOString() : undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
await mutate({ mutation: createContributionLink, variables })
|
const result = await mutate({ mutation: createContributionLink, variables })
|
||||||
|
return result.data.createContributionLink
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm'
|
||||||
|
|
||||||
|
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||||
|
export class User extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
pubKey: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||||
|
privKey: Buffer
|
||||||
|
|
||||||
|
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'first_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
firstName: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'last_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
lastName: string
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||||
|
password: BigInt
|
||||||
|
|
||||||
|
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
emailHash: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||||
|
emailChecked: boolean
|
||||||
|
|
||||||
|
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||||
|
language: string
|
||||||
|
|
||||||
|
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
|
||||||
|
isAdmin: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||||
|
referrerId?: number | null
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'contribution_link_id',
|
||||||
|
type: 'int',
|
||||||
|
unsigned: true,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
contributionLinkId?: number | null
|
||||||
|
|
||||||
|
@Column({ name: 'publisher_id', default: 0 })
|
||||||
|
publisherId: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'text',
|
||||||
|
name: 'passphrase',
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
passphrase: string
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export { User } from './0037-drop_user_setting_table/User'
|
export { User } from './0040-add_contribution_link_id_to_user/User'
|
||||||
|
|||||||
14
database/migrations/0040-add_contribution_link_id_to_user.ts
Normal file
14
database/migrations/0040-add_contribution_link_id_to_user.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/* MIGRATION TO ADD contribution_link_id FIELD TO users */
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `users` ADD COLUMN `contribution_link_id` int UNSIGNED DEFAULT NULL AFTER `referrer_id`;',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn('ALTER TABLE `users` DROP COLUMN `contribution_link_id`;')
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-database",
|
"name": "gradido-database",
|
||||||
"version": "1.9.0",
|
"version": "1.10.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",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bootstrap-vue-gradido-wallet",
|
"name": "bootstrap-vue-gradido-wallet",
|
||||||
"version": "1.9.0",
|
"version": "1.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node run/server.js",
|
"start": "node run/server.js",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 63 KiB |
133
frontend/src/components/LanguageSwitch2.spec.js
Normal file
133
frontend/src/components/LanguageSwitch2.spec.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import LanguageSwitch from './LanguageSwitch2'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const updateUserInfosMutationMock = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
updateUserInfos: {
|
||||||
|
validValues: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('LanguageSwitch', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
email: 'he@ho.he',
|
||||||
|
language: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$store: {
|
||||||
|
state,
|
||||||
|
commit: jest.fn(),
|
||||||
|
},
|
||||||
|
$i18n: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$apollo: {
|
||||||
|
mutate: updateUserInfosMutationMock,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(LanguageSwitch, { localVue, mocks })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the component', () => {
|
||||||
|
expect(wrapper.find('div.language-switch').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with locales en and de', () => {
|
||||||
|
describe('empty store', () => {
|
||||||
|
describe('navigator language is "en-US"', () => {
|
||||||
|
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||||
|
it('shows English as default navigator langauge', async () => {
|
||||||
|
languageGetter.mockReturnValue('en-US')
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('navigator language is "de-DE"', () => {
|
||||||
|
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||||
|
it('shows Deutsch as language ', async () => {
|
||||||
|
languageGetter.mockReturnValue('de-DE')
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('navigator language is "es-ES" (not supported)', () => {
|
||||||
|
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||||
|
it('shows English as language ', async () => {
|
||||||
|
languageGetter.mockReturnValue('es-ES')
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('no navigator langauge', () => {
|
||||||
|
const languageGetter = jest.spyOn(navigator, 'language', 'get')
|
||||||
|
it('shows English as language ', async () => {
|
||||||
|
languageGetter.mockReturnValue(null)
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('language "de" in store', () => {
|
||||||
|
it('shows Deutsch as language', async () => {
|
||||||
|
wrapper.vm.$store.state.language = 'de'
|
||||||
|
wrapper.vm.setCurrentLanguage()
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('language menu', () => {
|
||||||
|
it('has English and German as languages to choose', () => {
|
||||||
|
expect(wrapper.findAll('span.locales')).toHaveLength(2)
|
||||||
|
})
|
||||||
|
it('has English as first language to choose', () => {
|
||||||
|
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
|
||||||
|
})
|
||||||
|
it('has German as second language to choose', () => {
|
||||||
|
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('calls the API', () => {
|
||||||
|
it("with locale 'de'", () => {
|
||||||
|
wrapper.findAll('span.locales').at(1).trigger('click')
|
||||||
|
expect(updateUserInfosMutationMock).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
locale: 'de',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// it("with locale 'en'", () => {
|
||||||
|
// wrapper.findAll('span.locales').at(0).trigger('click')
|
||||||
|
// expect(updateUserInfosMutationMock).toBeCalledWith(
|
||||||
|
// expect.objectContaining({
|
||||||
|
// variables: {
|
||||||
|
// locale: 'en',
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -7,7 +7,7 @@
|
|||||||
class="pointer pr-3"
|
class="pointer pr-3"
|
||||||
:class="$store.state.language === lang.code ? 'c-blau' : 'c-grey'"
|
:class="$store.state.language === lang.code ? 'c-blau' : 'c-grey'"
|
||||||
>
|
>
|
||||||
{{ lang.name }}
|
<span class="locales">{{ lang.name }}</span>
|
||||||
<span class="ml-3">{{ locales.length - 1 > index ? $t('math.pipe') : '' }}</span>
|
<span class="ml-3">{{ locales.length - 1 > index ? $t('math.pipe') : '' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-information">
|
<div class="redeem-information">
|
||||||
<b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info">
|
<b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info">
|
||||||
<h1>
|
<h1 v-if="isContributionLink">
|
||||||
{{ firstName }}
|
{{ CONFIG.COMMUNITY_NAME }}
|
||||||
|
{{ $t('contribution-link.thanksYouWith') }} {{ amount | GDD }}
|
||||||
|
</h1>
|
||||||
|
<h1 v-else>
|
||||||
|
{{ user.firstName }}
|
||||||
{{ $t('transaction-link.send_you') }} {{ amount | GDD }}
|
{{ $t('transaction-link.send_you') }} {{ amount | GDD }}
|
||||||
</h1>
|
</h1>
|
||||||
<b>{{ memo }}</b>
|
<b>{{ memo }}</b>
|
||||||
@ -10,12 +14,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RedeemInformation',
|
name: 'RedeemInformation',
|
||||||
props: {
|
props: {
|
||||||
firstName: { type: String, required: true },
|
user: { type: Object, required: false },
|
||||||
amount: { type: String, required: true },
|
amount: { type: String, required: true },
|
||||||
memo: { type: String, required: true, default: '' },
|
memo: { type: String, required: true, default: '' },
|
||||||
|
isContributionLink: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
CONFIG,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-logged-out">
|
<div class="redeem-logged-out">
|
||||||
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" />
|
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
|
||||||
|
|
||||||
<b-jumbotron>
|
<b-jumbotron>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
@ -32,9 +32,8 @@ export default {
|
|||||||
RedeemInformation,
|
RedeemInformation,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, required: true },
|
linkData: { type: Object, required: true },
|
||||||
amount: { type: String, required: true },
|
isContributionLink: { type: Boolean, default: false },
|
||||||
memo: { type: String, required: true, default: '' },
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
login() {
|
login() {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-self-creator">
|
<div class="redeem-self-creator">
|
||||||
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" />
|
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
|
||||||
|
|
||||||
<b-jumbotron>
|
<b-jumbotron>
|
||||||
<div class="mb-3 text-center">
|
<div class="mb-3 text-center">
|
||||||
@ -23,9 +23,8 @@ export default {
|
|||||||
RedeemInformation,
|
RedeemInformation,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, required: true },
|
linkData: { type: Object, required: true },
|
||||||
amount: { type: String, required: true },
|
isContributionLink: { type: Boolean, default: false },
|
||||||
memo: { type: String, required: true, default: '' },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="redeem-valid">
|
<div class="redeem-valid">
|
||||||
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" />
|
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
|
||||||
<b-jumbotron>
|
<b-jumbotron>
|
||||||
<div class="mb-3 text-center">
|
<div class="mb-3 text-center">
|
||||||
<b-button variant="primary" @click="$emit('redeem-link', amount)" size="lg">
|
<b-button variant="primary" @click="$emit('redeem-link', linkData.amount)" size="lg">
|
||||||
{{ $t('gdd_per_link.redeem') }}
|
{{ $t('gdd_per_link.redeem') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
@ -19,9 +19,8 @@ export default {
|
|||||||
RedeemInformation,
|
RedeemInformation,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { type: Object, required: false },
|
linkData: { type: Object, required: true },
|
||||||
amount: { type: String, required: false },
|
isContributionLink: { type: Boolean, default: false },
|
||||||
memo: { type: String, required: false, default: '' },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -114,6 +114,7 @@ export const queryOptIn = gql`
|
|||||||
export const queryTransactionLink = gql`
|
export const queryTransactionLink = gql`
|
||||||
query($code: String!) {
|
query($code: String!) {
|
||||||
queryTransactionLink(code: $code) {
|
queryTransactionLink(code: $code) {
|
||||||
|
... on TransactionLink {
|
||||||
id
|
id
|
||||||
amount
|
amount
|
||||||
memo
|
memo
|
||||||
@ -127,6 +128,21 @@ export const queryTransactionLink = gql`
|
|||||||
email
|
email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
... on ContributionLink {
|
||||||
|
id
|
||||||
|
validTo
|
||||||
|
validFrom
|
||||||
|
amount
|
||||||
|
name
|
||||||
|
memo
|
||||||
|
cycle
|
||||||
|
createdAt
|
||||||
|
code
|
||||||
|
link
|
||||||
|
deletedAt
|
||||||
|
maxAmountPerMonth
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,9 @@
|
|||||||
"other-communities": "Weitere Gemeinschaften",
|
"other-communities": "Weitere Gemeinschaften",
|
||||||
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
|
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
|
||||||
},
|
},
|
||||||
|
"contribution-link": {
|
||||||
|
"thanksYouWith": "dankt dir mit"
|
||||||
|
},
|
||||||
"decay": {
|
"decay": {
|
||||||
"before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.",
|
"before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.",
|
||||||
"calculation_decay": "Berechnung der Vergänglichkeit",
|
"calculation_decay": "Berechnung der Vergänglichkeit",
|
||||||
|
|||||||
@ -26,6 +26,9 @@
|
|||||||
"other-communities": "Other communities",
|
"other-communities": "Other communities",
|
||||||
"switch-to-this-community": "Switch to this community"
|
"switch-to-this-community": "Switch to this community"
|
||||||
},
|
},
|
||||||
|
"contribution-link": {
|
||||||
|
"thanksYouWith": "thanks you with"
|
||||||
|
},
|
||||||
"decay": {
|
"decay": {
|
||||||
"before_startblock_transaction": "This transaction does not include decay.",
|
"before_startblock_transaction": "This transaction does not include decay.",
|
||||||
"calculation_decay": "Calculation of Decay",
|
"calculation_decay": "Calculation of Decay",
|
||||||
|
|||||||
@ -88,7 +88,11 @@ export default {
|
|||||||
? this.$t('message.checkEmail')
|
? this.$t('message.checkEmail')
|
||||||
: this.$t('message.reset')
|
: this.$t('message.reset')
|
||||||
this.messageButtonText = this.$t('login')
|
this.messageButtonText = this.$t('login')
|
||||||
|
if (this.$route.params.code) {
|
||||||
|
this.messageButtonLinktTo = `/login/${this.$route.params.code}`
|
||||||
|
} else {
|
||||||
this.messageButtonLinktTo = '/login'
|
this.messageButtonLinktTo = '/login'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
let errorMessage
|
let errorMessage
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const transactionLinkValidExpireDate = () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -82,6 +83,7 @@ describe('TransactionLink', () => {
|
|||||||
variables: {
|
variables: {
|
||||||
code: 'some-code',
|
code: 'some-code',
|
||||||
},
|
},
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -90,6 +92,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -120,6 +123,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -150,6 +154,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -177,9 +182,11 @@ describe('TransactionLink', () => {
|
|||||||
|
|
||||||
describe('no token in store', () => {
|
describe('no token in store', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mocks.$store.state.token = null
|
||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -213,6 +220,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -248,6 +256,7 @@ describe('TransactionLink', () => {
|
|||||||
apolloQueryMock.mockResolvedValue({
|
apolloQueryMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
queryTransactionLink: {
|
queryTransactionLink: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
id: 92,
|
id: 92,
|
||||||
amount: '22',
|
amount: '22',
|
||||||
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
|
||||||
@ -298,7 +307,7 @@ describe('TransactionLink', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('toasts a success message', () => {
|
it('toasts a success message', () => {
|
||||||
expect(mocks.$t).toBeCalledWith('gdd_per_link.redeemed', { n: '22' })
|
expect(mocks.$t).toBeCalledWith('gdd_per_link.redeem')
|
||||||
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ')
|
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="show-transaction-link-informations">
|
<div class="show-transaction-link-informations">
|
||||||
<div class="text-center"><b-img :src="img" fluid alt="logo"></b-img></div>
|
|
||||||
<b-container class="mt-4">
|
<b-container class="mt-4">
|
||||||
<transaction-link-item :type="itemType">
|
<transaction-link-item :type="itemType">
|
||||||
<template #LOGGED_OUT>
|
<template #LOGGED_OUT>
|
||||||
<redeem-logged-out v-bind="linkData" />
|
<redeem-logged-out :linkData="linkData" :isContributionLink="isContributionLink" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #SELF_CREATOR>
|
<template #SELF_CREATOR>
|
||||||
<redeem-self-creator v-bind="linkData" />
|
<redeem-self-creator :linkData="linkData" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #VALID>
|
<template #VALID>
|
||||||
<redeem-valid v-bind="linkData" @redeem-link="redeemLink" />
|
<redeem-valid
|
||||||
|
:linkData="linkData"
|
||||||
|
:isContributionLink="isContributionLink"
|
||||||
|
@redeem-link="redeemLink"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #TEXT>
|
<template #TEXT>
|
||||||
@ -44,6 +47,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
img: '/img/brand/green.png',
|
img: '/img/brand/green.png',
|
||||||
linkData: {
|
linkData: {
|
||||||
|
__typename: 'TransactionLink',
|
||||||
amount: '123.45',
|
amount: '123.45',
|
||||||
memo: 'memo',
|
memo: 'memo',
|
||||||
user: {
|
user: {
|
||||||
@ -57,6 +61,7 @@ export default {
|
|||||||
setTransactionLinkInformation() {
|
setTransactionLinkInformation() {
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.query({
|
.query({
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
query: queryTransactionLink,
|
query: queryTransactionLink,
|
||||||
variables: {
|
variables: {
|
||||||
code: this.$route.params.code,
|
code: this.$route.params.code,
|
||||||
@ -64,15 +69,16 @@ export default {
|
|||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.linkData = result.data.queryTransactionLink
|
this.linkData = result.data.queryTransactionLink
|
||||||
|
if (this.linkData.__typename === 'ContributionLink' && this.$store.state.token) {
|
||||||
|
this.mutationLink(this.linkData.amount)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.toastError(err.message)
|
this.toastError(err.message)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
redeemLink(amount) {
|
mutationLink(amount) {
|
||||||
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then(async (value) => {
|
this.$apollo
|
||||||
if (value)
|
|
||||||
await this.$apollo
|
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: redeemTransactionLink,
|
mutation: redeemTransactionLink,
|
||||||
variables: {
|
variables: {
|
||||||
@ -91,10 +97,17 @@ export default {
|
|||||||
this.toastError(err.message)
|
this.toastError(err.message)
|
||||||
this.$router.push('/overview')
|
this.$router.push('/overview')
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
redeemLink(amount) {
|
||||||
|
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then((value) => {
|
||||||
|
if (value) this.mutationLink(amount)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isContributionLink() {
|
||||||
|
return this.$route.params.code.search(/^CL-/) === 0
|
||||||
|
},
|
||||||
itemType() {
|
itemType() {
|
||||||
// link wurde gelöscht: am, von
|
// link wurde gelöscht: am, von
|
||||||
if (this.linkData.deletedAt) {
|
if (this.linkData.deletedAt) {
|
||||||
@ -124,16 +137,12 @@ export default {
|
|||||||
|
|
||||||
if (this.$store.state.token) {
|
if (this.$store.state.token) {
|
||||||
// logged in, nicht berechtigt einzulösen, eigener link
|
// logged in, nicht berechtigt einzulösen, eigener link
|
||||||
if (this.$store.state.email === this.linkData.user.email) {
|
if (this.linkData.user && this.$store.state.email === this.linkData.user.email) {
|
||||||
return `SELF_CREATOR`
|
return `SELF_CREATOR`
|
||||||
}
|
}
|
||||||
|
|
||||||
// logged in und berechtigt einzulösen
|
// logged in und berechtigt einzulösen
|
||||||
if (
|
if (!this.linkData.redeemedAt && !this.linkData.deletedAt) {
|
||||||
this.$store.state.email !== this.linkData.user.email &&
|
|
||||||
!this.linkData.redeemedAt &&
|
|
||||||
!this.linkData.deletedAt
|
|
||||||
) {
|
|
||||||
return `VALID`
|
return `VALID`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,11 @@ const authLink = new ApolloLink((operation, forward) => {
|
|||||||
|
|
||||||
const apolloClient = new ApolloClient({
|
const apolloClient = new ApolloClient({
|
||||||
link: authLink.concat(httpLink),
|
link: authLink.concat(httpLink),
|
||||||
cache: new InMemoryCache(),
|
cache: new InMemoryCache({
|
||||||
|
possibleTypes: {
|
||||||
|
QueryLinkResult: ['TransactionLink', 'ContributionLink'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const apolloProvider = new VueApollo({
|
export const apolloProvider = new VueApollo({
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido",
|
"name": "gradido",
|
||||||
"version": "1.9.0",
|
"version": "1.10.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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user