mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2501-feature-federation-implement-a-graphql-client-to-request-getpublickey
This commit is contained in:
commit
9769cf7610
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -437,7 +437,7 @@ jobs:
|
||||
report_name: Coverage Frontend
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 91
|
||||
min_coverage: 92
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
@ -4,12 +4,14 @@ export const communityStatistics = gql`
|
||||
query {
|
||||
communityStatistics {
|
||||
totalUsers
|
||||
activeUsers
|
||||
deletedUsers
|
||||
totalGradidoCreated
|
||||
totalGradidoDecayed
|
||||
totalGradidoAvailable
|
||||
totalGradidoUnbookedDecayed
|
||||
dynamicStatisticsFields {
|
||||
activeUsers
|
||||
totalGradidoAvailable
|
||||
totalGradidoUnbookedDecayed
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -17,12 +17,14 @@ const defaultData = () => {
|
||||
return {
|
||||
communityStatistics: {
|
||||
totalUsers: 3113,
|
||||
activeUsers: 1057,
|
||||
deletedUsers: 35,
|
||||
totalGradidoCreated: '4083774.05000000000000000000',
|
||||
totalGradidoDecayed: '-1062639.13634129622923372197',
|
||||
totalGradidoAvailable: '2513565.869444365732411569',
|
||||
totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
|
||||
dynamicStatisticsFields: {
|
||||
activeUsers: 1057,
|
||||
totalGradidoAvailable: '2513565.869444365732411569',
|
||||
totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,7 +31,9 @@ export default {
|
||||
return communityStatistics
|
||||
},
|
||||
update({ communityStatistics }) {
|
||||
this.statistics = communityStatistics
|
||||
const totals = { ...communityStatistics.dynamicStatisticsFields }
|
||||
this.statistics = { ...communityStatistics, ...totals }
|
||||
delete this.statistics.dynamicStatisticsFields
|
||||
},
|
||||
error({ message }) {
|
||||
this.toastError(message)
|
||||
|
||||
@ -2,13 +2,25 @@ import { ObjectType, Field } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class CommunityStatistics {
|
||||
@Field(() => Number)
|
||||
totalUsers: number
|
||||
|
||||
export class DynamicStatisticsFields {
|
||||
@Field(() => Number)
|
||||
activeUsers: number
|
||||
|
||||
@Field(() => Decimal)
|
||||
totalGradidoAvailable: Decimal
|
||||
|
||||
@Field(() => Decimal)
|
||||
totalGradidoUnbookedDecayed: Decimal
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class CommunityStatistics {
|
||||
@Field(() => Number)
|
||||
allUsers: number
|
||||
|
||||
@Field(() => Number)
|
||||
totalUsers: number
|
||||
|
||||
@Field(() => Number)
|
||||
deletedUsers: number
|
||||
|
||||
@ -18,9 +30,7 @@ export class CommunityStatistics {
|
||||
@Field(() => Decimal)
|
||||
totalGradidoDecayed: Decimal
|
||||
|
||||
@Field(() => Decimal)
|
||||
totalGradidoAvailable: Decimal
|
||||
|
||||
@Field(() => Decimal)
|
||||
totalGradidoUnbookedDecayed: Decimal
|
||||
// be carefull querying this, takes longer than 2 secs.
|
||||
@Field(() => DynamicStatisticsFields)
|
||||
dynamicStatisticsFields: DynamicStatisticsFields
|
||||
}
|
||||
|
||||
@ -557,7 +557,6 @@ export class ContributionResolver {
|
||||
): Promise<boolean> {
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
|
||||
try {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
@ -664,7 +663,6 @@ export class ContributionResolver {
|
||||
} finally {
|
||||
releaseLock()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -1,81 +1,113 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Resolver, Query, Authorized } from 'type-graphql'
|
||||
import { Resolver, Query, Authorized, FieldResolver } from 'type-graphql'
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
import { CommunityStatistics } from '@model/CommunityStatistics'
|
||||
import { CommunityStatistics, DynamicStatisticsFields } from '@model/CommunityStatistics'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
@Resolver()
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
@Resolver((of) => CommunityStatistics)
|
||||
export class StatisticsResolver {
|
||||
@Authorized([RIGHTS.COMMUNITY_STATISTICS])
|
||||
@Query(() => CommunityStatistics)
|
||||
async communityStatistics(): Promise<CommunityStatistics> {
|
||||
const allUsers = await DbUser.count({ withDeleted: true })
|
||||
const totalUsers = await DbUser.count()
|
||||
const deletedUsers = allUsers - totalUsers
|
||||
return new CommunityStatistics()
|
||||
}
|
||||
|
||||
@FieldResolver(() => Decimal)
|
||||
async allUsers(): Promise<number> {
|
||||
return await DbUser.count({ withDeleted: true })
|
||||
}
|
||||
|
||||
@FieldResolver()
|
||||
async totalUsers(): Promise<number> {
|
||||
return await DbUser.count()
|
||||
}
|
||||
|
||||
@FieldResolver()
|
||||
async deletedUsers(): Promise<number> {
|
||||
return (await this.allUsers()) - (await this.totalUsers())
|
||||
}
|
||||
|
||||
@FieldResolver()
|
||||
async totalGradidoCreated(): Promise<Decimal> {
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
try {
|
||||
await queryRunner.connect()
|
||||
const { totalGradidoCreated } = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('SUM(transaction.amount) AS totalGradidoCreated')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.typeId = 1')
|
||||
.getRawOne()
|
||||
return totalGradidoCreated
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
}
|
||||
|
||||
@FieldResolver()
|
||||
async totalGradidoDecayed(): Promise<Decimal> {
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
try {
|
||||
await queryRunner.connect()
|
||||
const { totalGradidoDecayed } = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('SUM(transaction.decay) AS totalGradidoDecayed')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.decay IS NOT NULL')
|
||||
.getRawOne()
|
||||
return totalGradidoDecayed
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
}
|
||||
|
||||
@FieldResolver()
|
||||
async dynamicStatisticsFields(): Promise<DynamicStatisticsFields> {
|
||||
let totalGradidoAvailable: Decimal = new Decimal(0)
|
||||
let totalGradidoUnbookedDecayed: Decimal = new Decimal(0)
|
||||
|
||||
const receivedCallDate = new Date()
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
try {
|
||||
await queryRunner.connect()
|
||||
|
||||
const lastUserTransactions = await queryRunner.manager
|
||||
.createQueryBuilder(DbUser, 'user')
|
||||
.select('transaction.balance', 'balance')
|
||||
.addSelect('transaction.balance_date', 'balanceDate')
|
||||
.innerJoin(DbTransaction, 'transaction', 'user.id = transaction.user_id')
|
||||
.where(
|
||||
`transaction.balance_date = (SELECT MAX(t.balance_date) FROM transactions AS t WHERE t.user_id = user.id)`,
|
||||
)
|
||||
.orderBy('transaction.balance_date', 'DESC')
|
||||
.addOrderBy('transaction.id', 'DESC')
|
||||
.getRawMany()
|
||||
const lastUserTransactions = await queryRunner.manager
|
||||
.createQueryBuilder(DbUser, 'user')
|
||||
.select('transaction.balance', 'balance')
|
||||
.addSelect('transaction.balance_date', 'balanceDate')
|
||||
.innerJoin(DbTransaction, 'transaction', 'user.id = transaction.user_id')
|
||||
.where(
|
||||
`transaction.balance_date = (SELECT MAX(t.balance_date) FROM transactions AS t WHERE t.user_id = user.id)`,
|
||||
)
|
||||
.orderBy('transaction.balance_date', 'DESC')
|
||||
.addOrderBy('transaction.id', 'DESC')
|
||||
.getRawMany()
|
||||
|
||||
const activeUsers = lastUserTransactions.length
|
||||
const activeUsers = lastUserTransactions.length
|
||||
|
||||
lastUserTransactions.forEach(({ balance, balanceDate }) => {
|
||||
const decay = calculateDecay(new Decimal(balance), new Date(balanceDate), receivedCallDate)
|
||||
if (decay) {
|
||||
totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString())
|
||||
totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString())
|
||||
lastUserTransactions.forEach(({ balance, balanceDate }) => {
|
||||
const decay = calculateDecay(new Decimal(balance), new Date(balanceDate), receivedCallDate)
|
||||
if (decay) {
|
||||
totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString())
|
||||
totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString())
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
activeUsers,
|
||||
totalGradidoAvailable,
|
||||
totalGradidoUnbookedDecayed,
|
||||
}
|
||||
})
|
||||
|
||||
const { totalGradidoCreated } = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('SUM(transaction.amount) AS totalGradidoCreated')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.typeId = 1')
|
||||
.getRawOne()
|
||||
|
||||
const { totalGradidoDecayed } = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('SUM(transaction.decay) AS totalGradidoDecayed')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.decay IS NOT NULL')
|
||||
.getRawOne()
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
return {
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
deletedUsers,
|
||||
totalGradidoCreated,
|
||||
totalGradidoDecayed,
|
||||
totalGradidoAvailable,
|
||||
totalGradidoUnbookedDecayed,
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
import { transactionLinkCode } from './TransactionLinkResolver'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { cleanDB, testEnvironment, resetToken } from '@test/helpers'
|
||||
import { cleanDB, testEnvironment, resetToken, resetEntity } from '@test/helpers'
|
||||
import { creationFactory } from '@/seeds/factory/creation'
|
||||
import { creations } from '@/seeds/creation/index'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
@ -50,238 +50,340 @@ afterAll(async () => {
|
||||
})
|
||||
|
||||
describe('TransactionLinkResolver', () => {
|
||||
// TODO: have this test separated into a transactionLink and a contributionLink part (if possible)
|
||||
describe('redeem daily Contribution Link', () => {
|
||||
const now = new Date()
|
||||
let contributionLink: DbContributionLink | undefined
|
||||
let contribution: UnconfirmedContribution | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
amount: new Decimal(5),
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
cycle: 'DAILY',
|
||||
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
|
||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a daily contribution link in the database', async () => {
|
||||
const cls = await DbContributionLink.find()
|
||||
expect(cls).toHaveLength(1)
|
||||
contributionLink = cls[0]
|
||||
expect(contributionLink).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
validFrom: new Date(now.getFullYear(), 0, 1),
|
||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0),
|
||||
cycle: 'DAILY',
|
||||
maxPerCycle: 1,
|
||||
totalMaxCountOfContribution: null,
|
||||
maxAccountBalance: null,
|
||||
minGapHours: null,
|
||||
createdAt: expect.any(Date),
|
||||
deletedAt: null,
|
||||
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
|
||||
linkEnabled: true,
|
||||
amount: expect.decimalEqual(5),
|
||||
maxAmountPerMonth: expect.decimalEqual(200),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('user has pending contribution of 1000 GDD', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
const result = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: new Decimal(1000),
|
||||
memo: 'I was brewing potions for the community the whole month',
|
||||
creationDate: now.toISOString(),
|
||||
},
|
||||
})
|
||||
contribution = result.data.createContribution
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has no pending contributions that would not allow to redeem the link', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: contribution ? contribution.id : -1,
|
||||
amount: new Decimal(800),
|
||||
memo: 'I was brewing potions for the community the whole month',
|
||||
creationDate: now.toISOString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('allows the user to redeem the contribution link', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
redeemTransactionLink: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
describe('after one day', () => {
|
||||
describe('redeemTransactionLink', () => {
|
||||
describe('contributionLink', () => {
|
||||
describe('input not valid', () => {
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers()
|
||||
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
|
||||
jest.runAllTimers()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('allows the user to redeem the contribution link again', async () => {
|
||||
it('throws error when link does not exists', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
redeemTransactionLink: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
code: 'CL-123456',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
'Creation from contribution link was not successful. Error: No contribution link found to given code: CL-123456',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('throws error when link is not valid yet', async () => {
|
||||
const now = new Date()
|
||||
const {
|
||||
data: { createContributionLink: contributionLink },
|
||||
} = await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
amount: new Decimal(5),
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
cycle: 'DAILY',
|
||||
validFrom: new Date(now.getFullYear() + 1, 0, 1).toISOString(),
|
||||
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + contributionLink.code,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link not valid yet',
|
||||
),
|
||||
],
|
||||
})
|
||||
await resetEntity(DbContributionLink)
|
||||
})
|
||||
|
||||
it('throws error when contributionLink cycle is invalid', async () => {
|
||||
const now = new Date()
|
||||
const {
|
||||
data: { createContributionLink: contributionLink },
|
||||
} = await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
amount: new Decimal(5),
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
cycle: 'INVALID',
|
||||
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
|
||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + contributionLink.code,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link has unknown cycle',
|
||||
),
|
||||
],
|
||||
})
|
||||
await resetEntity(DbContributionLink)
|
||||
})
|
||||
|
||||
it('throws error when link is no longer valid', async () => {
|
||||
const now = new Date()
|
||||
const {
|
||||
data: { createContributionLink: contributionLink },
|
||||
} = await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
amount: new Decimal(5),
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
cycle: 'DAILY',
|
||||
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
|
||||
validTo: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999).toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + contributionLink.code,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link is no longer valid',
|
||||
),
|
||||
],
|
||||
})
|
||||
await resetEntity(DbContributionLink)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('transaction links list', () => {
|
||||
const variables = {
|
||||
userId: 1, // dummy, may be replaced
|
||||
filters: null,
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
}
|
||||
// TODO: have this test separated into a transactionLink and a contributionLink part
|
||||
describe('redeem daily Contribution Link', () => {
|
||||
const now = new Date()
|
||||
let contributionLink: DbContributionLink | undefined
|
||||
let contribution: UnconfirmedContribution | undefined
|
||||
|
||||
// TODO: there is a test not cleaning up after itself! Fix it!
|
||||
beforeAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
amount: new Decimal(5),
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
cycle: 'DAILY',
|
||||
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
|
||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
it('has a daily contribution link in the database', async () => {
|
||||
const cls = await DbContributionLink.find()
|
||||
expect(cls).toHaveLength(1)
|
||||
contributionLink = cls[0]
|
||||
expect(contributionLink).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
validFrom: new Date(now.getFullYear(), 0, 1),
|
||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0),
|
||||
cycle: 'DAILY',
|
||||
maxPerCycle: 1,
|
||||
totalMaxCountOfContribution: null,
|
||||
maxAccountBalance: null,
|
||||
minGapHours: null,
|
||||
createdAt: expect.any(Date),
|
||||
deletedAt: null,
|
||||
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
|
||||
linkEnabled: true,
|
||||
amount: expect.decimalEqual(5),
|
||||
maxAmountPerMonth: expect.decimalEqual(200),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('user has pending contribution of 1000 GDD', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
const result = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: new Decimal(1000),
|
||||
memo: 'I was brewing potions for the community the whole month',
|
||||
creationDate: now.toISOString(),
|
||||
},
|
||||
})
|
||||
contribution = result.data.createContribution
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has no pending contributions that would not allow to redeem the link', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: contribution ? contribution.id : -1,
|
||||
amount: new Decimal(800),
|
||||
memo: 'I was brewing potions for the community the whole month',
|
||||
creationDate: now.toISOString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('allows the user to redeem the contribution link', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
redeemTransactionLink: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
describe('after one day', () => {
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers()
|
||||
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
|
||||
jest.runAllTimers()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('allows the user to redeem the contribution link again', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
redeemTransactionLink: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('transaction links list', () => {
|
||||
const variables = {
|
||||
userId: 1, // dummy, may be replaced
|
||||
filters: null,
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
}
|
||||
|
||||
// TODO: there is a test not cleaning up after itself! Fix it!
|
||||
beforeAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
@ -296,40 +398,22 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
// admin 'peter@lustig.de' has to exists for 'creationFactory'
|
||||
await userFactory(testEnv, peterLustig)
|
||||
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
variables.userId = user.id
|
||||
variables.pageSize = 25
|
||||
// bibi needs GDDs
|
||||
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await creationFactory(testEnv, bibisCreation!)
|
||||
// bibis transaktion links
|
||||
const bibisTransaktionLinks = transactionLinks.filter(
|
||||
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
|
||||
)
|
||||
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
|
||||
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
|
||||
}
|
||||
|
||||
// admin: only now log in
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
describe('authenticated', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('without any filters', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
@ -337,185 +421,235 @@ describe('TransactionLinkResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('all filters are null', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: null,
|
||||
withExpired: null,
|
||||
withRedeemed: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
// admin 'peter@lustig.de' has to exists for 'creationFactory'
|
||||
await userFactory(testEnv, peterLustig)
|
||||
|
||||
describe('filter with deleted', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 7,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
variables.userId = user.id
|
||||
variables.pageSize = 25
|
||||
// bibi needs GDDs
|
||||
const bibisCreation = creations.find(
|
||||
(creation) => creation.email === 'bibi@bloxberg.de',
|
||||
)
|
||||
})
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await creationFactory(testEnv, bibisCreation!)
|
||||
// bibis transaktion links
|
||||
const bibisTransaktionLinks = transactionLinks.filter(
|
||||
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
|
||||
)
|
||||
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
|
||||
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
|
||||
}
|
||||
|
||||
describe('filter by expired', () => {
|
||||
it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withExpired: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 7,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
// admin: only now log in
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
|
||||
describe.skip('filter by redeemed', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: null,
|
||||
withExpired: null,
|
||||
withRedeemed: true,
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('without any filters', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Yeah, eingelöst!',
|
||||
redeemedAt: expect.any(String),
|
||||
redeemedBy: expect.any(Number),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('all filters are null', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: null,
|
||||
withExpired: null,
|
||||
withRedeemed: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter with deleted', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 7,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter by expired', () => {
|
||||
it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withExpired: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 7,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
|
||||
describe.skip('filter by redeemed', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: null,
|
||||
withExpired: null,
|
||||
withRedeemed: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Yeah, eingelöst!',
|
||||
redeemedAt: expect.any(String),
|
||||
redeemedBy: expect.any(Number),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('transactionLinkCode', () => {
|
||||
const date = new Date()
|
||||
describe('transactionLinkCode', () => {
|
||||
const date = new Date()
|
||||
|
||||
it('returns a string of length 24', () => {
|
||||
expect(transactionLinkCode(date)).toHaveLength(24)
|
||||
})
|
||||
it('returns a string of length 24', () => {
|
||||
expect(transactionLinkCode(date)).toHaveLength(24)
|
||||
})
|
||||
|
||||
it('returns a string that ends with the hex value of date', () => {
|
||||
const regexp = new RegExp(date.getTime().toString(16) + '$')
|
||||
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
|
||||
it('returns a string that ends with the hex value of date', () => {
|
||||
const regexp = new RegExp(date.getTime().toString(16) + '$')
|
||||
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -170,148 +170,154 @@ export class TransactionLinkResolver {
|
||||
if (code.match(/^CL-/)) {
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
logger.info('redeem contribution link...')
|
||||
const now = new Date()
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
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')
|
||||
logger.info('redeem contribution link...')
|
||||
const now = new Date()
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
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 to given code: ${code}`)
|
||||
}
|
||||
}
|
||||
let alreadyRedeemed: DbContribution | undefined
|
||||
switch (contributionLink.cycle) {
|
||||
case ContributionCycleType.ONCE: {
|
||||
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.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 with rule ONCE already redeemed by user with id',
|
||||
user.id,
|
||||
'contribution link is no longer valid. Valid to: ',
|
||||
contributionLink.validTo,
|
||||
)
|
||||
throw new Error('Contribution link already redeemed')
|
||||
throw new Error('Contribution link is no longer valid')
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContributionCycleType.DAILY: {
|
||||
const start = new Date()
|
||||
start.setHours(0, 0, 0, 0)
|
||||
const end = new Date()
|
||||
end.setHours(23, 59, 59, 999)
|
||||
alreadyRedeemed = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contribution')
|
||||
.from(DbContribution, 'contribution')
|
||||
.where(
|
||||
`contribution.contributionLinkId = :linkId AND contribution.userId = :id
|
||||
AND Date(contribution.confirmedAt) BETWEEN :start AND :end`,
|
||||
{
|
||||
let alreadyRedeemed: DbContribution | undefined
|
||||
switch (contributionLink.cycle) {
|
||||
case ContributionCycleType.ONCE: {
|
||||
alreadyRedeemed = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contribution')
|
||||
.from(DbContribution, 'contribution')
|
||||
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
||||
linkId: contributionLink.id,
|
||||
id: user.id,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
)
|
||||
.getOne()
|
||||
if (alreadyRedeemed) {
|
||||
logger.error(
|
||||
'contribution link with rule DAILY already redeemed by user with id',
|
||||
user.id,
|
||||
)
|
||||
throw new Error('Contribution link already redeemed today')
|
||||
})
|
||||
.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')
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContributionCycleType.DAILY: {
|
||||
const start = new Date()
|
||||
start.setHours(0, 0, 0, 0)
|
||||
const end = new Date()
|
||||
end.setHours(23, 59, 59, 999)
|
||||
alreadyRedeemed = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contribution')
|
||||
.from(DbContribution, 'contribution')
|
||||
.where(
|
||||
`contribution.contributionLinkId = :linkId AND contribution.userId = :id
|
||||
AND Date(contribution.confirmedAt) BETWEEN :start AND :end`,
|
||||
{
|
||||
linkId: contributionLink.id,
|
||||
id: user.id,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
)
|
||||
.getOne()
|
||||
if (alreadyRedeemed) {
|
||||
logger.error(
|
||||
'contribution link with rule DAILY already redeemed by user with id',
|
||||
user.id,
|
||||
)
|
||||
throw new Error('Contribution link already redeemed today')
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
||||
throw new Error('Contribution link has unknown cycle')
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
||||
throw new Error('Contribution link has unknown cycle')
|
||||
|
||||
const creations = await getUserCreation(user.id, clientTimezoneOffset)
|
||||
logger.info('open creations', creations)
|
||||
validateContribution(creations, contributionLink.amount, now, clientTimezoneOffset)
|
||||
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
|
||||
contribution.contributionType = ContributionType.LINK
|
||||
contribution.contributionStatus = ContributionStatus.CONFIRMED
|
||||
|
||||
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.id', '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()
|
||||
}
|
||||
|
||||
const creations = await getUserCreation(user.id, clientTimezoneOffset)
|
||||
logger.info('open creations', creations)
|
||||
validateContribution(creations, contributionLink.amount, now, clientTimezoneOffset)
|
||||
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
|
||||
contribution.contributionType = ContributionType.LINK
|
||||
contribution.contributionStatus = ContributionStatus.CONFIRMED
|
||||
|
||||
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.id', '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()
|
||||
releaseLock()
|
||||
}
|
||||
return true
|
||||
|
||||
@ -45,29 +45,28 @@ export const executeTransaction = async (
|
||||
recipient: dbUser,
|
||||
transactionLink?: dbTransactionLink | null,
|
||||
): Promise<boolean> => {
|
||||
logger.info(
|
||||
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
|
||||
)
|
||||
|
||||
if (sender.id === recipient.id) {
|
||||
logger.error(`Sender and Recipient are the same.`)
|
||||
throw new Error('Sender and Recipient are the same.')
|
||||
}
|
||||
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
}
|
||||
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
|
||||
try {
|
||||
logger.info(
|
||||
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
|
||||
)
|
||||
|
||||
if (sender.id === recipient.id) {
|
||||
logger.error(`Sender and Recipient are the same.`)
|
||||
throw new Error('Sender and Recipient are the same.')
|
||||
}
|
||||
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
}
|
||||
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(
|
||||
@ -187,10 +186,10 @@ export const executeTransaction = async (
|
||||
})
|
||||
}
|
||||
logger.info(`finished executeTransaction successfully`)
|
||||
return true
|
||||
} finally {
|
||||
releaseLock()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
|
||||
@ -11,7 +11,7 @@ body {
|
||||
|
||||
.bg-gradient {
|
||||
background: rgb(4 112 6);
|
||||
background: linear-gradient(90deg, rgb(4 112 6 / 100%) 73%, rgb(197 141 56 / 100%) 100%);
|
||||
background: linear-gradient(90deg, rgb(4 112 6 / 100%) 22%, rgb(197 141 56 / 100%) 98%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ describe('ContributionForm', () => {
|
||||
date: '',
|
||||
memo: '',
|
||||
amount: '',
|
||||
hours: 0,
|
||||
},
|
||||
isThisMonth: true,
|
||||
minimalDate: new Date(),
|
||||
@ -22,6 +23,7 @@ describe('ContributionForm', () => {
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$n: jest.fn((n) => n),
|
||||
$store: {
|
||||
state: {
|
||||
creation: ['1000', '1000', '1000'],
|
||||
@ -375,6 +377,7 @@ describe('ContributionForm', () => {
|
||||
date: now,
|
||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||
amount: '200',
|
||||
hours: 0,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
|
||||
@ -3,13 +3,12 @@
|
||||
<b-form
|
||||
ref="form"
|
||||
@submit.prevent="submit"
|
||||
class="border p-3 bg-white appBoxShadow gradido-border-radius"
|
||||
class="p-3 bg-white appBoxShadow gradido-border-radius"
|
||||
>
|
||||
<label>{{ $t('contribution.selectDate') }}</label>
|
||||
<b-form-datepicker
|
||||
id="contribution-date"
|
||||
v-model="form.date"
|
||||
size="lg"
|
||||
:locale="$i18n.locale"
|
||||
:max="maximalDate"
|
||||
:min="minimalDate"
|
||||
@ -22,40 +21,39 @@
|
||||
<template #nav-prev-year><span></span></template>
|
||||
<template #nav-next-year><span></span></template>
|
||||
</b-form-datepicker>
|
||||
<div v-if="validMaxGDD > 0">
|
||||
<input-textarea
|
||||
id="contribution-memo"
|
||||
v-model="form.memo"
|
||||
:name="$t('form.message')"
|
||||
:label="$t('contribution.activity')"
|
||||
:placeholder="$t('contribution.yourActivity')"
|
||||
:rules="{ required: true, min: 5, max: 255 }"
|
||||
/>
|
||||
<input-hour
|
||||
v-model="form.hours"
|
||||
:name="$t('form.hours')"
|
||||
:label="$t('form.hours')"
|
||||
placeholder="0.5"
|
||||
:rules="{
|
||||
required: true,
|
||||
min: 0.5,
|
||||
max: validMaxTime,
|
||||
gddCreationTime: [0.5, validMaxTime],
|
||||
}"
|
||||
:validMaxTime="validMaxTime"
|
||||
@updateAmount="updateAmount"
|
||||
></input-hour>
|
||||
<input-amount
|
||||
id="contribution-amount"
|
||||
v-model="form.amount"
|
||||
:name="$t('form.amount')"
|
||||
:label="$t('form.amount')"
|
||||
placeholder="20"
|
||||
:rules="{ required: true, gddSendAmount: [20, validMaxGDD] }"
|
||||
typ="ContributionForm"
|
||||
></input-amount>
|
||||
</div>
|
||||
<div v-else class="mb-5">{{ $t('contribution.exhausted') }}</div>
|
||||
|
||||
<input-textarea
|
||||
id="contribution-memo"
|
||||
v-model="form.memo"
|
||||
:name="$t('form.message')"
|
||||
:label="$t('contribution.activity')"
|
||||
:placeholder="$t('contribution.yourActivity')"
|
||||
:rules="{ required: true, min: 5, max: 255 }"
|
||||
/>
|
||||
<input-hour
|
||||
v-model="form.hours"
|
||||
:name="$t('form.hours')"
|
||||
:label="$t('form.hours')"
|
||||
placeholder="0.5"
|
||||
:rules="{
|
||||
required: true,
|
||||
min: 0.5,
|
||||
max: validMaxTime,
|
||||
gddCreationTime: [0.5, validMaxTime],
|
||||
}"
|
||||
:validMaxTime="validMaxTime"
|
||||
@updateAmount="updateAmount"
|
||||
></input-hour>
|
||||
<input-amount
|
||||
id="contribution-amount"
|
||||
v-model="form.amount"
|
||||
:name="$t('form.amount')"
|
||||
:label="$t('form.amount')"
|
||||
placeholder="20"
|
||||
:rules="{ required: true, gddSendAmount: [20, validMaxGDD] }"
|
||||
typ="ContributionForm"
|
||||
></input-amount>
|
||||
|
||||
<b-row class="mt-5">
|
||||
<b-col>
|
||||
<b-button type="reset" variant="secondary" @click="reset" data-test="button-cancel">
|
||||
@ -111,7 +109,7 @@ export default {
|
||||
this.form.id = null
|
||||
this.form.date = ''
|
||||
this.form.memo = ''
|
||||
this.form.hours = 0.0
|
||||
this.form.hours = 0
|
||||
this.form.amount = ''
|
||||
},
|
||||
},
|
||||
|
||||
@ -48,7 +48,7 @@ describe('ContributionListItem', () => {
|
||||
|
||||
it('is x-circle when deletedAt is present', async () => {
|
||||
await wrapper.setProps({ deletedAt: new Date().toISOString() })
|
||||
expect(wrapper.vm.icon).toBe('x-circle')
|
||||
expect(wrapper.vm.icon).toBe('trash')
|
||||
})
|
||||
|
||||
it('is check when confirmedAt is present', async () => {
|
||||
|
||||
@ -30,7 +30,10 @@
|
||||
<div class="small">
|
||||
{{ $t('creation') }} {{ $t('(') }}{{ amount / 20 }} {{ $t('h') }}{{ $t(')') }}
|
||||
</div>
|
||||
<div class="font-weight-bold">{{ amount | GDD }}</div>
|
||||
<div v-if="state === 'DELETED'" class="small">
|
||||
{{ $t('contribution.deleted') }}
|
||||
</div>
|
||||
<div v-else class="font-weight-bold">{{ amount | GDD }}</div>
|
||||
</b-col>
|
||||
<b-col cols="12" md="1" lg="1" class="text-right align-items-center">
|
||||
<div v-if="messagesCount > 0" @click="visible = !visible">
|
||||
@ -168,7 +171,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
if (this.deletedAt) return 'x-circle'
|
||||
if (this.deletedAt) return 'trash'
|
||||
if (this.confirmedAt) return 'check'
|
||||
if (this.state === 'IN_PROGRESS') return 'question-circle'
|
||||
return 'bell-fill'
|
||||
|
||||
@ -35,12 +35,15 @@
|
||||
<b-col offset="2">{{ $t('form.new_balance') }}</b-col>
|
||||
<b-col>{{ (balance - amount) | GDD }}</b-col>
|
||||
</b-row>
|
||||
<b-row class="mt-5 p-5">
|
||||
<b-col>
|
||||
<b-button @click="$emit('on-back')">{{ $t('back') }}</b-button>
|
||||
<b-row class="mt-5">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<b-button block @click="$emit('on-back')" class="mb-3 mb-md-0 mb-lg-0">
|
||||
{{ $t('back') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-col cols="12" md="6" lg="6" class="text-lg-right">
|
||||
<b-button
|
||||
block
|
||||
class="send-button"
|
||||
variant="gradido"
|
||||
:disabled="disabled"
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row class="mt-5 pr-3 text-color-gdd-yellow h3">
|
||||
<b-row class="mt-5 text-color-gdd-yellow h3">
|
||||
<b-col cols="2" class="text-right">
|
||||
<b-icon class="text-color-gdd-yellow" icon="droplet-half"></b-icon>
|
||||
</b-col>
|
||||
@ -39,12 +39,15 @@
|
||||
<b-col offset="2">{{ $t('form.new_balance') }}</b-col>
|
||||
<b-col>{{ (balance - amount) | GDD }}</b-col>
|
||||
</b-row>
|
||||
<b-row class="mt-5 p-5">
|
||||
<b-col>
|
||||
<b-button @click="$emit('on-back')">{{ $t('back') }}</b-button>
|
||||
<b-row class="mt-5">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<b-button block @click="$emit('on-back')" class="mb-3 mb-md-0 mb-lg-0">
|
||||
{{ $t('back') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-col cols="12" md="6" lg="6" class="text-lg-right">
|
||||
<b-button
|
||||
block
|
||||
variant="gradido"
|
||||
:disabled="disabled"
|
||||
@click="$emit('send-transaction'), (disabled = true)"
|
||||
|
||||
@ -92,14 +92,20 @@
|
||||
<div v-if="!!isBalanceDisabled" class="text-danger mt-5">
|
||||
{{ $t('form.no_gdd_available') }}
|
||||
</div>
|
||||
<b-row v-else class="test-buttons mt-5">
|
||||
<b-col>
|
||||
<b-button type="reset" variant="secondary" @click="onReset">
|
||||
<b-row v-else class="test-buttons mt-3">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<b-button
|
||||
block
|
||||
type="reset"
|
||||
variant="secondary"
|
||||
@click="onReset"
|
||||
class="mb-3 mb-md-0 mb-lg-0"
|
||||
>
|
||||
{{ $t('form.reset') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button type="submit" variant="gradido">
|
||||
<b-col cols="12" md="6" lg="6" class="text-lg-right">
|
||||
<b-button block type="submit" variant="gradido">
|
||||
{{ $t('form.check_now') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
|
||||
@ -26,45 +26,46 @@
|
||||
</template>
|
||||
</transaction-list-item>
|
||||
</div>
|
||||
<div v-if="transactionCount > 0" class="h4 m-3">{{ $t('lastMonth') }}</div>
|
||||
<div v-for="({ id, typeId }, index) in transactions" :key="`l2-` + id">
|
||||
<transaction-list-item
|
||||
v-if="typeId !== 'DECAY'"
|
||||
:typeId="typeId"
|
||||
class="pointer mb-4 bg-white appBoxShadow gradido-border-radius p-3 test-list-group-item"
|
||||
>
|
||||
<template #SEND>
|
||||
<transaction-send
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
<div class="mt-3">
|
||||
<div v-for="({ id, typeId }, index) in transactions" :key="`l2-` + id">
|
||||
<transaction-list-item
|
||||
v-if="typeId !== 'DECAY'"
|
||||
:typeId="typeId"
|
||||
class="pointer mb-3 bg-white appBoxShadow gradido-border-radius p-3 test-list-group-item"
|
||||
>
|
||||
<template #SEND>
|
||||
<transaction-send
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #RECEIVE>
|
||||
<transaction-receive
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
<template #RECEIVE>
|
||||
<transaction-receive
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #CREATION>
|
||||
<transaction-creation
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
<template #CREATION>
|
||||
<transaction-creation
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #LINK_SUMMARY>
|
||||
<transaction-link-summary
|
||||
v-bind="transactions[index]"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
@update-transactions="updateTransactions"
|
||||
/>
|
||||
</template>
|
||||
</transaction-list-item>
|
||||
<template #LINK_SUMMARY>
|
||||
<transaction-link-summary
|
||||
v-bind="transactions[index]"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
@update-transactions="updateTransactions"
|
||||
/>
|
||||
</template>
|
||||
</transaction-list-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<b-pagination
|
||||
|
||||
@ -9,10 +9,10 @@ describe('InputAmount', () => {
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$n: jest.fn((n) => n),
|
||||
$i18n: {
|
||||
locale: jest.fn(() => 'en'),
|
||||
},
|
||||
$n: jest.fn((n) => String(n)),
|
||||
$route: {
|
||||
params: {},
|
||||
},
|
||||
@ -46,13 +46,14 @@ describe('InputAmount', () => {
|
||||
|
||||
describe('amount normalization', () => {
|
||||
describe('if invalid', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setProps({ value: '12m34' })
|
||||
valid = false
|
||||
})
|
||||
|
||||
it('is not normalized', () => {
|
||||
wrapper.vm.normalizeAmount(valid)
|
||||
expect(wrapper.vm.amountValue).toBe(0.0)
|
||||
wrapper.vm.normalizeAmount(false)
|
||||
expect(wrapper.vm.currentValue).toBe('12m34')
|
||||
})
|
||||
})
|
||||
|
||||
@ -97,13 +98,14 @@ describe('InputAmount', () => {
|
||||
|
||||
describe('amount normalization', () => {
|
||||
describe('if invalid', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setProps({ value: '12m34' })
|
||||
valid = false
|
||||
})
|
||||
|
||||
it('is not normalized', () => {
|
||||
wrapper.vm.normalizeAmount(valid)
|
||||
expect(wrapper.vm.amountValue).toBe(0.0)
|
||||
expect(wrapper.vm.currentValue).toBe('12m34')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
trim
|
||||
v-focus="amountFocused"
|
||||
@focus="amountFocused = true"
|
||||
@blur="normalizeAmount(true)"
|
||||
@blur="normalizeAmount(valid)"
|
||||
:disabled="disabled"
|
||||
autocomplete="off"
|
||||
></b-form-input>
|
||||
@ -90,5 +90,8 @@ export default {
|
||||
this.currentValue = this.$n(this.amountValue, 'ungroupedDecimal')
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.value !== '') this.normalizeAmount(true)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -74,7 +74,7 @@ describe('InputHour', () => {
|
||||
it('emits input with new value', async () => {
|
||||
await wrapper.find('input').setValue('12')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')).toEqual([['12']])
|
||||
expect(wrapper.emitted('input')).toEqual([[12]])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -32,11 +32,11 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
name: { type: String, required: true, default: 'Time' },
|
||||
label: { type: String, required: true, default: 'Time' },
|
||||
placeholder: { type: String, required: true, default: 'Time' },
|
||||
name: { type: String, required: true },
|
||||
label: { type: String, required: true },
|
||||
placeholder: { type: String, required: true },
|
||||
value: { type: Number, required: true, default: 0 },
|
||||
validMaxTime: { type: Number, required: true, default: 0 },
|
||||
validMaxTime: { type: Number, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -50,7 +50,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
this.$emit('input', this.currentValue)
|
||||
this.$emit('input', Number(this.currentValue))
|
||||
},
|
||||
value() {
|
||||
if (this.value !== this.currentValue) this.currentValue = this.value
|
||||
|
||||
@ -1,64 +1,49 @@
|
||||
<template>
|
||||
<div class="navbar-component position-sticky">
|
||||
<b-navbar toggleable="lg" class="pr-4">
|
||||
<b-navbar-brand>
|
||||
<b-img
|
||||
class="imgLogo mt-lg--2 mt-3 mb-3 d-none d-lg-block zindex10"
|
||||
:src="logo"
|
||||
width=""
|
||||
alt="..."
|
||||
/>
|
||||
<b-button v-b-toggle.sidebar-mobile class="d-block d-lg-none">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</b-button>
|
||||
</b-navbar-brand>
|
||||
|
||||
<router-link to="/settings" class="d-block d-lg-none">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="mr-3">
|
||||
<avatar
|
||||
:username="username.username"
|
||||
:initials="username.initials"
|
||||
:color="'#fff'"
|
||||
:size="61"
|
||||
></avatar>
|
||||
<div class="navbar-component">
|
||||
<div class="navbar-element">
|
||||
<b-navbar toggleable="lg" class="pr-4">
|
||||
<b-navbar-brand>
|
||||
<b-img
|
||||
class="imgLogo mt-lg--2 mt-3 mb-3 d-none d-lg-block zindex10"
|
||||
:src="logo"
|
||||
width=""
|
||||
alt="..."
|
||||
/>
|
||||
<div v-b-toggle.sidebar-mobile variant="link" class="d-block d-lg-none">
|
||||
<span class="navbar-toggler-icon h2"></span>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<b-img class="sheet-img position-absolute zindex-1" :src="sheet"></b-img>
|
||||
<b-collapse id="nav-collapse" is-nav class="ml-5">
|
||||
<b-navbar-nav class="ml-auto" right>
|
||||
<div class="mb-2">
|
||||
<router-link to="/settings">
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="mr-3">
|
||||
<avatar
|
||||
:username="username.username"
|
||||
:initials="username.initials"
|
||||
:color="'#fff'"
|
||||
:size="81"
|
||||
></avatar>
|
||||
</div>
|
||||
<div>
|
||||
<div data-test="navbar-item-username">{{ username.username }}</div>
|
||||
</b-navbar-brand>
|
||||
|
||||
<div class="text-right" data-test="navbar-item-email">
|
||||
{{ $store.state.email }}
|
||||
</div>
|
||||
</div>
|
||||
<b-img class="sheet-img position-absolute zindex-1" :src="sheet"></b-img>
|
||||
|
||||
<b-navbar-nav class="ml-auto" right>
|
||||
<router-link to="/settings">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="mr-3">
|
||||
<avatar
|
||||
:username="username.username"
|
||||
:initials="username.initials"
|
||||
:color="'#fff'"
|
||||
:size="61"
|
||||
></avatar>
|
||||
</div>
|
||||
<div>
|
||||
<div data-test="navbar-item-username">{{ username.username }}</div>
|
||||
|
||||
<div class="text-right" data-test="navbar-item-email">
|
||||
{{ $store.state.email }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</b-navbar-nav>
|
||||
</b-collapse>
|
||||
</b-navbar>
|
||||
<!-- <div class="alertBox">
|
||||
</b-navbar>
|
||||
<!-- <div class="alertBox">
|
||||
<b-alert show dismissible variant="light" class="nav-alert text-dark">
|
||||
<small>{{ $t('1000thanks') }}</small>
|
||||
</b-alert>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -91,6 +76,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar-element {
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
font-family: 'Open Sans', sans-serif !important;
|
||||
height: 150px;
|
||||
@ -126,7 +115,7 @@ button.navbar-toggler > span.navbar-toggler-icon {
|
||||
}
|
||||
@media screen and (max-width: 1170px) {
|
||||
.sheet-img {
|
||||
left: 40%;
|
||||
left: 20%;
|
||||
}
|
||||
.alertBox {
|
||||
position: static;
|
||||
@ -136,10 +125,15 @@ button.navbar-toggler > span.navbar-toggler-icon {
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 450px) {
|
||||
.sheet-img {
|
||||
left: 37%;
|
||||
max-width: 61%;
|
||||
.navbar-element {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background-color: #f5f5f5e6;
|
||||
}
|
||||
.sheet-img {
|
||||
left: 5%;
|
||||
max-width: 61%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<div id="component-sidebar">
|
||||
<div id="side-menu" ref="sideMenu" class="gradido-border-radius appBoxShadow pt-2">
|
||||
<div
|
||||
id="side-menu"
|
||||
ref="sideMenu"
|
||||
class="gradido-border-radius pt-2 bg-white"
|
||||
:class="shadow ? 'appBoxShadow' : ''"
|
||||
>
|
||||
<div class="mb-3 mt-3">
|
||||
<b-nav vertical class="w-200">
|
||||
<b-nav-item to="/overview" class="mb-3" active-class="activeRoute">
|
||||
@ -19,7 +24,7 @@
|
||||
<b-icon icon="layers" aria-hidden="true"></b-icon>
|
||||
<span class="ml-2">{{ $t('gdt.gdt') }}</span>
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/community#my" class="" active-class="activeRoute">
|
||||
<b-nav-item to="/community" class="" active-class="activeRoute">
|
||||
<b-icon icon="people" aria-hidden="true"></b-icon>
|
||||
<span class="ml-2">{{ $t('creation') }}</span>
|
||||
</b-nav-item>
|
||||
@ -55,6 +60,9 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
props: {
|
||||
shadow: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@ -1,23 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-sidebar id="sidebar-mobile" bg-variant="f5" :backdrop="true">
|
||||
<b-sidebar id="sidebar-mobile" :backdrop="true" bg-variant="transparent">
|
||||
<div class="px-3 py-2">
|
||||
<sidebar @admin="$emit('admin')" @logout="$emit('logout')" />
|
||||
<sidebar @admin="$emit('admin')" @logout="$emit('logout')" :shadow="false" />
|
||||
</div>
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="mr-auto">{{ avatarLongName }}</div>
|
||||
<div class="small">
|
||||
<small>{{ $store.state.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="d-flex bg-light">
|
||||
<strong class="mr-auto p-2">{{ $t('send_gdd') }}</strong>
|
||||
<b-button to="/send"><b-icon icon="arrow-right"></b-icon></b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-sidebar>
|
||||
</div>
|
||||
</template>
|
||||
@ -29,10 +15,5 @@ export default {
|
||||
components: {
|
||||
Sidebar,
|
||||
},
|
||||
computed: {
|
||||
avatarLongName() {
|
||||
return `${this.$store.state.firstName} ${this.$store.state.lastName}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -7,23 +7,25 @@
|
||||
>
|
||||
<b-card-body>
|
||||
<b-card-title class="h2">{{ item.text }}</b-card-title>
|
||||
</b-card-body>
|
||||
<b-card-footer class="bg-transparent">
|
||||
|
||||
<div class="h3">{{ item.date }}</div>
|
||||
|
||||
<b-row class="my-5">
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<div class="h3">{{ item.date }}</div>
|
||||
<b-col>
|
||||
{{ item.extra }}
|
||||
</b-col>
|
||||
<b-col cols="12" md="6" lg="6">
|
||||
<div class="text-right">
|
||||
</b-row>
|
||||
|
||||
<b-row class="my-5">
|
||||
<b-col cols="12">
|
||||
<div class="text-lg-right">
|
||||
<b-button variant="gradido" :href="item.url" target="_blank">
|
||||
{{ $t('auth.left.learnMore') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
{{ item.extra }}
|
||||
</b-card-footer>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="nav-community">
|
||||
<div class="nav-community container">
|
||||
<b-row class="nav-row">
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<b-btn active-class="btn-active" block variant="link" to="#edit">
|
||||
<b-col cols="12" lg="4" md="4" class="px-0">
|
||||
<b-btn active-class="btn-active" block variant="link" to="/community#edit">
|
||||
<b-icon icon="pencil" class="mr-2" />
|
||||
{{ $t('community.submitContribution') }}
|
||||
</b-btn>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<b-btn active-class="btn-active" block variant="link" to="#my">
|
||||
<b-col cols="12" lg="4" md="4" class="px-0">
|
||||
<b-btn active-class="btn-active" block variant="link" to="/community#my">
|
||||
<b-icon icon="person" class="mr-2" />
|
||||
{{ $t('community.myContributions') }}
|
||||
</b-btn>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<b-btn active-class="btn-active" block variant="link" to="#all">
|
||||
<b-col cols="12" lg="4" md="4" class="px-0">
|
||||
<b-btn active-class="btn-active" block variant="link" to="/community#all">
|
||||
<b-icon icon="people" class="mr-2" />
|
||||
{{ $t('community.community') }}
|
||||
</b-btn>
|
||||
|
||||
@ -22,6 +22,10 @@
|
||||
<b-icon icon="x-circle" variant="danger"></b-icon>
|
||||
{{ $t('contribution.alert.rejected') }}
|
||||
</li>
|
||||
<li>
|
||||
<b-icon icon="trash" variant="danger"></b-icon>
|
||||
{{ $t('contribution.alert.deleted') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="hash === '#all'" show fade variant="secondary" class="text-dark">
|
||||
|
||||
@ -1,41 +1,46 @@
|
||||
<template>
|
||||
<div class="userdata-card">
|
||||
<b-row>
|
||||
<b-col class="centerPerMargin">
|
||||
<avatar
|
||||
:username="username.username"
|
||||
:initials="username.initials"
|
||||
:color="'#fff'"
|
||||
:size="90"
|
||||
></avatar>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-card class="border-0">
|
||||
<b-container class="justify-content-center mt-md-5">
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div class="text-center font-weight-bold">
|
||||
{{ $n(balance, 'decimal') }}
|
||||
</div>
|
||||
<div class="text-center">{{ $t('GDD') }}</div>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<div class="text-center font-weight-bold">
|
||||
{{ transactionCount }}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
{{ $t('navigation.transactions') }}
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<div class="text-center font-weight-bold">{{ CONFIG.COMMUNITY_NAME }}</div>
|
||||
<div class="text-center">
|
||||
{{ $t('community.community') }}
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</b-card>
|
||||
<div class="centerPerMargin">
|
||||
<avatar
|
||||
:username="username.username"
|
||||
:initials="username.initials"
|
||||
:color="'#fff'"
|
||||
:size="90"
|
||||
></avatar>
|
||||
</div>
|
||||
|
||||
<div class="justify-content-center mt-5 mb-5">
|
||||
<b-row align-v="stretch">
|
||||
<b-col cols="4">
|
||||
<div class="text-center font-weight-bold">
|
||||
{{ $n(balance, 'decimal') }}
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="4">
|
||||
<div class="text-center font-weight-bold">
|
||||
{{ transactionCount }}
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="4">
|
||||
<div class="text-center font-weight-bold">{{ CONFIG.COMMUNITY_NAME }}</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col cols="4">
|
||||
<div class="text-center">{{ $t('GDD') }}</div>
|
||||
</b-col>
|
||||
<b-col cols="4">
|
||||
<div class="text-center">
|
||||
{{ $t('navigation.transactions') }}
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="4">
|
||||
<div class="text-center">
|
||||
{{ $t('community.community') }}
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -70,4 +75,14 @@ export default {
|
||||
.centerPerMargin {
|
||||
padding-left: 44%;
|
||||
}
|
||||
@media screen and (max-width: 850px) {
|
||||
.centerPerMargin {
|
||||
padding-left: 38%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 450px) {
|
||||
.centerPerMargin {
|
||||
padding-left: 34%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -13,7 +13,6 @@ export const verifyLogin = gql`
|
||||
hasElopage
|
||||
publisherId
|
||||
isAdmin
|
||||
creation
|
||||
hideAmountGDD
|
||||
hideAmountGDT
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ const routerPushMock = jest.fn()
|
||||
const stubs = {
|
||||
RouterLink: RouterLinkStub,
|
||||
RouterView: true,
|
||||
LastTransactions: true,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
@ -29,6 +30,9 @@ const mocks = {
|
||||
meta: {
|
||||
hideFooter: false,
|
||||
},
|
||||
path: {
|
||||
replace: jest.fn(),
|
||||
},
|
||||
},
|
||||
$router: {
|
||||
push: routerPushMock,
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<div v-if="skeleton">
|
||||
<skeleton-overview />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-else class="mx--3 mx-lg-0">
|
||||
<!-- navbar -->
|
||||
<b-row>
|
||||
<b-col>
|
||||
@ -13,7 +13,7 @@
|
||||
<mobile-sidebar @admin="admin" @logout="logout" />
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<b-row>
|
||||
<b-row class="breadcrumb">
|
||||
<b-col cols="10" offset-lg="2">
|
||||
<breadcrumb />
|
||||
</b-col>
|
||||
@ -35,7 +35,87 @@
|
||||
:balance="balance"
|
||||
:GdtBalance="GdtBalance"
|
||||
:totalUsers="totalUsers"
|
||||
/>
|
||||
>
|
||||
<template #overview>
|
||||
<b-row>
|
||||
<b-col cols="12" lg="5">
|
||||
<div>
|
||||
<gdd-amount :balance="balance" :showStatus="false" :badgeShow="false" />
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="7">
|
||||
<div>
|
||||
<community-member :totalUsers="totalUsers" />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
<template #send>
|
||||
<b-row>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<gdd-amount
|
||||
:balance="balance"
|
||||
:badge="true"
|
||||
:showStatus="true"
|
||||
:badgeShow="false"
|
||||
/>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="gdt">
|
||||
<gdt-amount :GdtBalance="GdtBalance" :badgeShow="false" />
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
<template #transactions>
|
||||
<b-row>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="transactions">
|
||||
<gdd-amount :balance="balance" :showStatus="true" />
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="gdt">
|
||||
<gdt-amount :GdtBalance="GdtBalance" />
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
<template #gdt>
|
||||
<b-row>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="transactions">
|
||||
<gdd-amount :balance="balance" :showStatus="false" />
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="gdt">
|
||||
<gdt-amount
|
||||
:badge="true"
|
||||
:showStatus="true"
|
||||
:GdtBalance="GdtBalance"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
<template #community>
|
||||
<nav-community />
|
||||
</template>
|
||||
<template #settings></template>
|
||||
</content-header>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
@ -46,11 +126,24 @@
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
@set-tunneled-email="setTunneledEmail"
|
||||
/>
|
||||
>
|
||||
<template #transactions>
|
||||
<last-transactions
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
<template #community>
|
||||
<contribution-info />
|
||||
</template>
|
||||
<template #empty />
|
||||
</right-side>
|
||||
</b-col>
|
||||
<b-col cols="12">
|
||||
<!-- router-view -->
|
||||
<div class="main-content mt-3">
|
||||
<div class="main-content mt-lg-3 mt-0">
|
||||
<fade-transition :duration="200" origin="center top" mode="out-in">
|
||||
<router-view
|
||||
ref="router-view"
|
||||
@ -75,7 +168,20 @@
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
@set-tunneled-email="setTunneledEmail"
|
||||
/>
|
||||
>
|
||||
<template #transactions>
|
||||
<last-transactions
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
<template #community>
|
||||
<contribution-info />
|
||||
</template>
|
||||
<template #empty />
|
||||
</right-side>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
@ -102,6 +208,12 @@ import { logout } from '@/graphql/mutations'
|
||||
import ContentFooter from '@/components/ContentFooter.vue'
|
||||
import { FadeTransition } from 'vue2-transitions'
|
||||
import CONFIG from '@/config'
|
||||
import GddAmount from '@/components/Template/ContentHeader/GddAmount.vue'
|
||||
import GdtAmount from '@/components/Template/ContentHeader/GdtAmount.vue'
|
||||
import CommunityMember from '@/components/Template/ContentHeader/CommunityMember.vue'
|
||||
import NavCommunity from '@/components/Template/ContentHeader/NavCommunity.vue'
|
||||
import LastTransactions from '@/components/Template/RightSide/LastTransactions.vue'
|
||||
import ContributionInfo from '@/components/Template/RightSide/ContributionInfo.vue'
|
||||
|
||||
export default {
|
||||
name: 'DashboardLayout',
|
||||
@ -116,6 +228,12 @@ export default {
|
||||
ContentFooter,
|
||||
FadeTransition,
|
||||
Breadcrumb,
|
||||
GddAmount,
|
||||
GdtAmount,
|
||||
CommunityMember,
|
||||
NavCommunity,
|
||||
LastTransactions,
|
||||
ContributionInfo,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -218,7 +336,9 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* frontend/public/img/svg/Gradido_Blaetter_Mainpage.svg */
|
||||
.breadcrumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
.main-page {
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
@ -251,4 +371,10 @@ export default {
|
||||
.navbar-toggler-icon {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(4, 112, 6, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
.breadcrumb {
|
||||
padding-top: 60px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,116 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="path === '/overview'">
|
||||
<b-row>
|
||||
<b-col cols="12" lg="5">
|
||||
<div>
|
||||
<gdd-amount :balance="balance" :showStatus="false" :path="path" :badgeShow="false" />
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="7">
|
||||
<div>
|
||||
<community-member :totalUsers="totalUsers" />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
<!-- <div v-if="path === '/storys'"></div>
|
||||
<div v-if="path === '/addresses'"></div> -->
|
||||
<div v-if="path === '/send'">
|
||||
<b-row>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<gdd-amount :balance="balance" :badge="true" :showStatus="true" :badgeShow="false" />
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="gdt">
|
||||
<gdt-amount :GdtBalance="GdtBalance" :badgeShow="false" />
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
<div v-if="path === '/transactions'">
|
||||
<b-row>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="transactions">
|
||||
<gdd-amount :balance="balance" :showStatus="true" />
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="gdt">
|
||||
<gdt-amount :GdtBalance="GdtBalance" />
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
<div v-if="path === '/gdt'">
|
||||
<b-row>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="transactions">
|
||||
<gdd-amount :balance="balance" :showStatus="false" />
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6">
|
||||
<div>
|
||||
<router-link to="gdt">
|
||||
<gdt-amount :badge="true" :showStatus="true" :GdtBalance="GdtBalance" />
|
||||
</router-link>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
<!-- <div v-if="path === '/profile'">
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div class="p-4 bg-white appBoxShadow gradido-border-radius">
|
||||
<b-row>
|
||||
<b-col cols="8" class="h3">Zeige deiner Community wer du bist.</b-col>
|
||||
<b-col cols="4" class="text-small text-muted">vor 4 Stunden geändert</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col cols="2" class=""><b-avatar size="72px" rounded="lg"></b-avatar></b-col>
|
||||
<b-col cols="10" class="">Text</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div> -->
|
||||
<div v-if="path === '/community'"><nav-community /></div>
|
||||
<div v-if="path === '/settings'"></div>
|
||||
<slot :name="path" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GddAmount from '@/components/Template/ContentHeader/GddAmount.vue'
|
||||
import GdtAmount from '@/components/Template/ContentHeader/GdtAmount.vue'
|
||||
import CommunityMember from '@/components/Template/ContentHeader/CommunityMember.vue'
|
||||
import NavCommunity from '@/components/Template/ContentHeader/NavCommunity.vue'
|
||||
export default {
|
||||
name: 'ContentHeader',
|
||||
components: {
|
||||
GddAmount,
|
||||
GdtAmount,
|
||||
CommunityMember,
|
||||
NavCommunity,
|
||||
},
|
||||
props: {
|
||||
balance: { type: Number, required: true },
|
||||
GdtBalance: { type: Number, required: true },
|
||||
totalUsers: { type: Number, required: true },
|
||||
},
|
||||
|
||||
computed: {
|
||||
path() {
|
||||
return this.$route.path
|
||||
return this.$route.path.replace(/^\//, '')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,157 +1,23 @@
|
||||
<template>
|
||||
<div class="right-side mt-3 mt-lg-0">
|
||||
<b-container v-if="path === '/overview'" fluid="md">
|
||||
<!-- <b-row>
|
||||
<b-col>
|
||||
<div class="p-4">
|
||||
<favourites />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row> -->
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div>
|
||||
<last-transactions
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-container>
|
||||
<slot :name="name" />
|
||||
</b-container>
|
||||
<!-- <b-container v-if="path === '/storys'">
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div class="p-4">
|
||||
<favourites />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="mt-3 mt-lg-5">
|
||||
<b-col>
|
||||
<div class="p-4 h-100">
|
||||
<top-storys-by-month />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container> -->
|
||||
<!-- <b-container v-if="path === '/addresses'">favourites ride side</b-container> -->
|
||||
<b-container v-if="path === '/send'">
|
||||
<!-- <b-row>
|
||||
<b-col>
|
||||
<div class="p-4">
|
||||
<favourites />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row> -->
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div>
|
||||
<last-transactions
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
<b-container v-if="path === '/transactions'">
|
||||
<!-- <b-row>
|
||||
<b-col>
|
||||
<div class="p-4">
|
||||
<favourites />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row> -->
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div>
|
||||
<last-transactions
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
<b-container v-if="path === '/gdt'">
|
||||
<!-- <b-row>
|
||||
<b-col>
|
||||
<div class="p-4">
|
||||
<favourites />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row> -->
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div>
|
||||
<last-transactions
|
||||
:transactions="transactions"
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
<!-- <b-container v-if="path === '/profile'">
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div class="p-4">
|
||||
<favourites />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="mt-3 mt-lg-5">
|
||||
<b-col>
|
||||
<div class="p-4 h-100">
|
||||
<your-overview />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container> -->
|
||||
<b-container v-if="path === '/community'">
|
||||
<contribution-info />
|
||||
<!-- <last-contributions class="mt-5" /> -->
|
||||
</b-container>
|
||||
<b-container v-if="path === '/settings'"></b-container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import LastTransactions from '@/components/Template/RightSide/LastTransactions.vue'
|
||||
// import Favourites from '@/components/Template/RightSide/Favourites.vue'
|
||||
// import TopStorysByMonth from '@/components/Template/RightSide/TopStorysByMonth.vue'
|
||||
import ContributionInfo from '@/components/Template/RightSide/ContributionInfo.vue'
|
||||
// import LastContributions from '@/components/Template/RightSide/LastContributions.vue'
|
||||
// import YourOverview from '@/components/Template/RightSide/YourOverview.vue'
|
||||
|
||||
export default {
|
||||
name: 'RightSide',
|
||||
components: {
|
||||
LastTransactions,
|
||||
// Favourites,
|
||||
// TopStorysByMonth,
|
||||
// LastContributions,
|
||||
ContributionInfo,
|
||||
// YourOverview,
|
||||
},
|
||||
props: {
|
||||
transactions: {
|
||||
default: () => [],
|
||||
},
|
||||
transactionCount: { type: Number, default: 0 },
|
||||
transactionLinkCount: { type: Number, default: 0 },
|
||||
},
|
||||
computed: {
|
||||
path() {
|
||||
return this.$route.path
|
||||
name() {
|
||||
switch (this.$route.path.replace(/^\//, '')) {
|
||||
case 'settings':
|
||||
return 'empty'
|
||||
case 'community':
|
||||
return 'community'
|
||||
default:
|
||||
return 'transactions'
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"answerQuestion": "Bitte beantworte die Rückfrage!",
|
||||
"communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.",
|
||||
"confirm": "bestätigt",
|
||||
"deleted": "gelöscht",
|
||||
"in_progress": "Es gibt eine Rückfrage der Moderatoren.",
|
||||
"myContributionNoteList": "Eingereichte Beiträge, die noch nicht bestätigt wurden, kannst du jederzeit bearbeiten oder löschen.",
|
||||
"pending": "Eingereicht und wartet auf Bestätigung",
|
||||
@ -47,7 +48,6 @@
|
||||
},
|
||||
"delete": "Beitrag löschen! Bist du sicher?",
|
||||
"deleted": "Der Beitrag wurde gelöscht! Wird aber sichtbar bleiben.",
|
||||
"exhausted": "Für diesen Monat kannst du nichts mehr schöpfen.",
|
||||
"formText": {
|
||||
"bringYourTalentsTo": "Bring dich mit deinen Talenten in die Gemeinschaft ein! Dein freiwilliges Engagement honorieren wir mit 20 GDD pro Stunde bis maximal 1.000 GDD im Monat.",
|
||||
"describeYourCommunity": "Beschreibe deine Gemeinwohl-Tätigkeit mit Angabe der Stunden und trage einen Betrag von 20 GDD pro Stunde ein! Nach Bestätigung durch einen Moderator wird der Betrag deinem Konto gutgeschrieben.",
|
||||
@ -215,7 +215,6 @@
|
||||
},
|
||||
"h": "h",
|
||||
"language": "Sprache",
|
||||
"lastMonth": "letzter Monat",
|
||||
"link-load": "den letzten Link nachladen | die letzten {n} Links nachladen | weitere {n} Links nachladen",
|
||||
"login": "Anmelden",
|
||||
"math": {
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"answerQuestion": "Please answer the question",
|
||||
"communityNoteList": "Here you will find all submitted and confirmed contributions from all members of this community.",
|
||||
"confirm": "confirmed",
|
||||
"deleted": "deleted",
|
||||
"in_progress": "There is a question from the moderators.",
|
||||
"myContributionNoteList": "You can edit or delete entries that have not yet been confirmed at any time.",
|
||||
"pending": "Submitted and waiting for confirmation",
|
||||
@ -47,7 +48,6 @@
|
||||
},
|
||||
"delete": "Delete Contribution! Are you sure?",
|
||||
"deleted": "The contribution has been deleted! But it will remain visible.",
|
||||
"exhausted": "You cannot create anything more for this month.",
|
||||
"formText": {
|
||||
"bringYourTalentsTo": "Bring your talents to the community! Your voluntary commitment will be rewarded with 20 GDD per hour up to a maximum of 1,000 GDD per month.",
|
||||
"describeYourCommunity": "Describe your community service activity with hours and enter an amount of 20 GDD per hour! After confirmation by a moderator, the amount will be credited to your account.",
|
||||
@ -215,7 +215,6 @@
|
||||
},
|
||||
"h": "h",
|
||||
"language": "Language",
|
||||
"lastMonth": "Last month",
|
||||
"link-load": "Load the last link | Load the last {n} links | Load more {n} links",
|
||||
"login": "Sign in",
|
||||
"math": {
|
||||
|
||||
@ -4,33 +4,13 @@ import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
|
||||
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
||||
import { listContributions, listAllContributions } from '@/graphql/queries'
|
||||
|
||||
import VueRouter from 'vue-router'
|
||||
import routes from '../routes/routes'
|
||||
|
||||
const localVue = global.localVue
|
||||
localVue.use(VueRouter)
|
||||
|
||||
const mockStoreDispach = jest.fn()
|
||||
const apolloQueryMock = jest.fn()
|
||||
const apolloMutationMock = jest.fn()
|
||||
const apolloRefetchMock = jest.fn()
|
||||
|
||||
const router = new VueRouter({
|
||||
base: '/',
|
||||
routes,
|
||||
linkActiveClass: 'active',
|
||||
mode: 'history',
|
||||
// scrollBehavior: (to, from, savedPosition) => {
|
||||
// if (savedPosition) {
|
||||
// return savedPosition
|
||||
// }
|
||||
// if (to.hash) {
|
||||
// return { selector: to.hash }
|
||||
// }
|
||||
// return { x: 0, y: 0 }
|
||||
// },
|
||||
})
|
||||
|
||||
describe('Community', () => {
|
||||
let wrapper
|
||||
|
||||
@ -55,12 +35,17 @@ describe('Community', () => {
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
$router: {
|
||||
push: jest.fn(),
|
||||
},
|
||||
$route: {
|
||||
hash: 'my',
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Community, {
|
||||
localVue,
|
||||
router,
|
||||
mocks,
|
||||
})
|
||||
}
|
||||
@ -299,13 +284,6 @@ describe('Community', () => {
|
||||
it('verifies the login (to get the new creations available)', () => {
|
||||
expect(apolloRefetchMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('set all data to the default values)', () => {
|
||||
expect(wrapper.vm.form.id).toBe(null)
|
||||
expect(wrapper.vm.form.date).toBe('')
|
||||
expect(wrapper.vm.form.memo).toBe('')
|
||||
expect(wrapper.vm.form.amount).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with error', () => {
|
||||
|
||||
@ -118,7 +118,7 @@ export default {
|
||||
if (num !== 0) {
|
||||
this.form = {
|
||||
id: null,
|
||||
date: '',
|
||||
date: new Date(),
|
||||
memo: '',
|
||||
hours: 0,
|
||||
amount: '',
|
||||
@ -280,7 +280,7 @@ export default {
|
||||
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
|
||||
this.tabIndex = 1
|
||||
if (this.$route.hash !== '#my') {
|
||||
this.$router.push({ path: '#my' })
|
||||
this.$router.push({ path: '/community#my' })
|
||||
}
|
||||
this.toastInfo('Du hast eine Rückfrage auf eine Contribution. Bitte beantworte diese!')
|
||||
}
|
||||
@ -318,6 +318,7 @@ export default {
|
||||
})
|
||||
this.updateTransactions(0)
|
||||
this.tabIndex = 1
|
||||
this.$router.push({ path: '/community#my' })
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<gdd-send :currentTransactionStep="currentTransactionStep" class="pt-3 mt--3">
|
||||
<gdd-send :currentTransactionStep="currentTransactionStep">
|
||||
<template #transactionForm>
|
||||
<transaction-form
|
||||
v-bind="transactionData"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container bg-white appBoxShadow gradido-border-radius p-3 mt--3">
|
||||
<div class="container bg-white appBoxShadow p-3 mt--3">
|
||||
<user-card :balance="balance" :transactionCount="transactionCount"></user-card>
|
||||
<user-data />
|
||||
<hr />
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Transactions from './Transactions'
|
||||
import { GdtEntryType } from '@/graphql/enums'
|
||||
import { listGDTEntriesQuery } from '@/graphql/queries'
|
||||
|
||||
import { toastErrorSpy } from '@test/testSetup'
|
||||
|
||||
@ -45,8 +46,8 @@ describe('Transactions', () => {
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Transactions, { localVue, mocks })
|
||||
const Wrapper = (propsData = {}) => {
|
||||
return mount(Transactions, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -77,147 +78,111 @@ describe('Transactions', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.skip('renders the transaction gradido transform table', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setData({
|
||||
gdt: true,
|
||||
})
|
||||
it('renders the transaction gradido transform table when gdt is true', async () => {
|
||||
await wrapper.setProps({
|
||||
gdt: true,
|
||||
})
|
||||
expect(wrapper.findComponent({ name: 'GdtTransactionList' }).exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe.skip('tabs', () => {
|
||||
it('shows the GDD transactions by default', () => {
|
||||
expect(wrapper.findAll('div[role="tabpanel"]').at(0).isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not show the GDT transactions by default', () => {
|
||||
expect(wrapper.findAll('div[role="tabpanel"]').at(1).isVisible()).toBeFalsy()
|
||||
})
|
||||
|
||||
describe('click on GDT tab', () => {
|
||||
describe('server returns valid data', () => {
|
||||
beforeEach(() => {
|
||||
apolloMock.mockResolvedValue({
|
||||
data: {
|
||||
listGDTEntries: {
|
||||
count: 4,
|
||||
gdtEntries: [
|
||||
{
|
||||
id: 1,
|
||||
amount: 100,
|
||||
gdt: 1700,
|
||||
factor: 17,
|
||||
comment: '',
|
||||
date: '2021-05-02T17:20:11+00:00',
|
||||
gdtEntryType: GdtEntryType.FORM,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
amount: 1810,
|
||||
gdt: 362,
|
||||
factor: 0.2,
|
||||
comment: 'Dezember 20',
|
||||
date: '2020-12-31T12:00:00+00:00',
|
||||
gdtEntryType: GdtEntryType.GLOBAL_MODIFICATOR,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
amount: 100,
|
||||
gdt: 1700,
|
||||
factor: 17,
|
||||
comment: '',
|
||||
date: '2020-05-07T17:00:00+00:00',
|
||||
gdtEntryType: GdtEntryType.FORM,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
amount: 100,
|
||||
gdt: 110,
|
||||
factor: 22,
|
||||
comment: '',
|
||||
date: '2020-04-10T13:28:00+00:00',
|
||||
gdtEntryType: GdtEntryType.ELOPAGE_PUBLISHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
wrapper.findAll('li[ role="presentation"]').at(1).find('a').trigger('click')
|
||||
})
|
||||
|
||||
it('does not show the GDD transactions', () => {
|
||||
expect(wrapper.findAll('div[role="tabpanel"]').at(0).isVisible()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('shows the GDT transactions', () => {
|
||||
expect(wrapper.findAll('div[role="tabpanel"]').at(1).isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('scrolls to (0, 0) after API call', () => {
|
||||
expect(windowScrollToMock).toBeCalledWith(0, 0)
|
||||
})
|
||||
|
||||
describe('click on GDD tab', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.findAll('li[ role="presentation"]').at(0).find('a').trigger('click')
|
||||
})
|
||||
|
||||
it('shows the GDD transactions', () => {
|
||||
expect(wrapper.findAll('div[role="tabpanel"]').at(0).isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not show the GDT', () => {
|
||||
expect(wrapper.findAll('div[role="tabpanel"]').at(1).isVisible()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('server returns error', () => {
|
||||
beforeEach(() => {
|
||||
apolloMock.mockRejectedValue({ message: 'Ouch!' })
|
||||
wrapper.findAll('li[ role="presentation"]').at(1).find('a').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||
})
|
||||
|
||||
it('sets transactionGdtCount to -1', () => {
|
||||
expect(wrapper.vm.transactionGdtCount).toBe(-1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('update currentPage', () => {
|
||||
describe('update gdt with success', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper.setData({
|
||||
currentPage: 2,
|
||||
apolloMock.mockResolvedValue({
|
||||
data: {
|
||||
listGDTEntries: {
|
||||
count: 4,
|
||||
gdtEntries: [
|
||||
{
|
||||
id: 1,
|
||||
amount: 100,
|
||||
gdt: 1700,
|
||||
factor: 17,
|
||||
comment: '',
|
||||
date: '2021-05-02T17:20:11+00:00',
|
||||
gdtEntryType: GdtEntryType.FORM,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
amount: 1810,
|
||||
gdt: 362,
|
||||
factor: 0.2,
|
||||
comment: 'Dezember 20',
|
||||
date: '2020-12-31T12:00:00+00:00',
|
||||
gdtEntryType: GdtEntryType.GLOBAL_MODIFICATOR,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
amount: 100,
|
||||
gdt: 1700,
|
||||
factor: 17,
|
||||
comment: '',
|
||||
date: '2020-05-07T17:00:00+00:00',
|
||||
gdtEntryType: GdtEntryType.FORM,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
amount: 100,
|
||||
gdt: 110,
|
||||
factor: 22,
|
||||
comment: '',
|
||||
date: '2020-04-10T13:28:00+00:00',
|
||||
gdtEntryType: GdtEntryType.ELOPAGE_PUBLISHER,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
wrapper = Wrapper({ gdt: true })
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
expect(apolloMock).toBeCalledWith({
|
||||
query: listGDTEntriesQuery,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('does not show the GDD transactions', () => {
|
||||
expect(wrapper.findAll('div.gdd-transaction-list').exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('shows the GDT transactions', () => {
|
||||
expect(wrapper.findAll('div.gdt-transaction-list').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('scrolls to (0, 0) after API call', () => {
|
||||
expect(windowScrollToMock).toBeCalledWith(0, 0)
|
||||
})
|
||||
|
||||
describe('update current page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper.vm.currentPage = 2
|
||||
})
|
||||
|
||||
it('calls the API again', () => {
|
||||
expect(apolloMock).toBeCalledWith({
|
||||
query: listGDTEntriesQuery,
|
||||
variables: {
|
||||
currentPage: 2,
|
||||
pageSize: 25,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update gdt with error', () => {
|
||||
beforeEach(() => {
|
||||
apolloMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
wrapper = Wrapper({ gdt: true })
|
||||
})
|
||||
|
||||
it('toasts the error', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -56,7 +56,6 @@ export const loadAllRules = (i18nCallback) => {
|
||||
|
||||
extend('gddCreationTime', {
|
||||
validate(value, { min, max }) {
|
||||
if (value) value = value.replace(',', '.')
|
||||
return value >= min && value <= max
|
||||
},
|
||||
params: ['min', 'max'],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user