mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2547-normalized-amount-transaction-is-processed-again
This commit is contained in:
commit
aadd2149aa
@ -557,7 +557,6 @@ export class ContributionResolver {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
// acquire lock
|
// acquire lock
|
||||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||||
const contribution = await DbContribution.findOne(id)
|
const contribution = await DbContribution.findOne(id)
|
||||||
@ -664,7 +663,6 @@ export class ContributionResolver {
|
|||||||
} finally {
|
} finally {
|
||||||
releaseLock()
|
releaseLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
import { transactionLinkCode } from './TransactionLinkResolver'
|
import { transactionLinkCode } from './TransactionLinkResolver'
|
||||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||||
import { cleanDB, testEnvironment, resetToken } from '@test/helpers'
|
import { cleanDB, testEnvironment, resetToken, resetEntity } from '@test/helpers'
|
||||||
import { creationFactory } from '@/seeds/factory/creation'
|
import { creationFactory } from '@/seeds/factory/creation'
|
||||||
import { creations } from '@/seeds/creation/index'
|
import { creations } from '@/seeds/creation/index'
|
||||||
import { userFactory } from '@/seeds/factory/user'
|
import { userFactory } from '@/seeds/factory/user'
|
||||||
@ -50,238 +50,340 @@ afterAll(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('TransactionLinkResolver', () => {
|
describe('TransactionLinkResolver', () => {
|
||||||
// TODO: have this test separated into a transactionLink and a contributionLink part (if possible)
|
describe('redeemTransactionLink', () => {
|
||||||
describe('redeem daily Contribution Link', () => {
|
describe('contributionLink', () => {
|
||||||
const now = new Date()
|
describe('input not valid', () => {
|
||||||
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', () => {
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.useFakeTimers()
|
|
||||||
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
|
|
||||||
jest.runAllTimers()
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
it('throws error when link does not exists', async () => {
|
||||||
jest.useRealTimers()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows the user to redeem the contribution link again', async () => {
|
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: redeemTransactionLink,
|
mutation: redeemTransactionLink,
|
||||||
variables: {
|
variables: {
|
||||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
code: 'CL-123456',
|
||||||
},
|
|
||||||
}),
|
|
||||||
).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({
|
).resolves.toMatchObject({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
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', () => {
|
// TODO: have this test separated into a transactionLink and a contributionLink part
|
||||||
const variables = {
|
describe('redeem daily Contribution Link', () => {
|
||||||
userId: 1, // dummy, may be replaced
|
const now = new Date()
|
||||||
filters: null,
|
let contributionLink: DbContributionLink | undefined
|
||||||
currentPage: 1,
|
let contribution: UnconfirmedContribution | undefined
|
||||||
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({
|
|
||||||
query: listTransactionLinksAdmin,
|
|
||||||
variables,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
errors: [new GraphQLError('401 Unauthorized')],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('authenticated', () => {
|
|
||||||
describe('without admin rights', () => {
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
user = await userFactory(testEnv, bibiBloxberg)
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
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 () => {
|
it('has a daily contribution link in the database', async () => {
|
||||||
await cleanDB()
|
const cls = await DbContributionLink.find()
|
||||||
resetToken()
|
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 () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
@ -296,40 +398,22 @@ describe('TransactionLinkResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with admin rights', () => {
|
describe('authenticated', () => {
|
||||||
beforeAll(async () => {
|
describe('without admin rights', () => {
|
||||||
// admin 'peter@lustig.de' has to exists for 'creationFactory'
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, peterLustig)
|
user = await userFactory(testEnv, bibiBloxberg)
|
||||||
|
await mutate({
|
||||||
user = await userFactory(testEnv, bibiBloxberg)
|
mutation: login,
|
||||||
variables.userId = user.id
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
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_' },
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await cleanDB()
|
await cleanDB()
|
||||||
resetToken()
|
resetToken()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('without any filters', () => {
|
it('returns an error', async () => {
|
||||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: listTransactionLinksAdmin,
|
query: listTransactionLinksAdmin,
|
||||||
@ -337,185 +421,235 @@ describe('TransactionLinkResolver', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: {
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
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('all filters are null', () => {
|
describe('with admin rights', () => {
|
||||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
beforeAll(async () => {
|
||||||
await expect(
|
// admin 'peter@lustig.de' has to exists for 'creationFactory'
|
||||||
query({
|
await userFactory(testEnv, peterLustig)
|
||||||
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', () => {
|
user = await userFactory(testEnv, bibiBloxberg)
|
||||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
variables.userId = user.id
|
||||||
await expect(
|
variables.pageSize = 25
|
||||||
query({
|
// bibi needs GDDs
|
||||||
query: listTransactionLinksAdmin,
|
const bibisCreation = creations.find(
|
||||||
variables: {
|
(creation) => creation.email === 'bibi@bloxberg.de',
|
||||||
...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),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
})
|
// 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', () => {
|
// admin: only now log in
|
||||||
it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
|
await mutate({
|
||||||
await expect(
|
mutation: login,
|
||||||
query({
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
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
|
afterAll(async () => {
|
||||||
describe.skip('filter by redeemed', () => {
|
await cleanDB()
|
||||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
resetToken()
|
||||||
await expect(
|
})
|
||||||
query({
|
|
||||||
query: listTransactionLinksAdmin,
|
describe('without any filters', () => {
|
||||||
variables: {
|
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||||
...variables,
|
await expect(
|
||||||
filters: {
|
query({
|
||||||
withDeleted: null,
|
query: listTransactionLinksAdmin,
|
||||||
withExpired: null,
|
variables,
|
||||||
withRedeemed: true,
|
}),
|
||||||
|
).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: {
|
describe('all filters are null', () => {
|
||||||
linkCount: 6,
|
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||||
linkList: expect.arrayContaining([
|
await expect(
|
||||||
expect.not.objectContaining({
|
query({
|
||||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
query: listTransactionLinksAdmin,
|
||||||
createdAt: expect.any(String),
|
variables: {
|
||||||
}),
|
...variables,
|
||||||
expect.objectContaining({
|
filters: {
|
||||||
memo: 'Yeah, eingelöst!',
|
withDeleted: null,
|
||||||
redeemedAt: expect.any(String),
|
withExpired: null,
|
||||||
redeemedBy: expect.any(Number),
|
withRedeemed: null,
|
||||||
}),
|
},
|
||||||
expect.not.objectContaining({
|
|
||||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
|
||||||
deletedAt: expect.any(String),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
}),
|
).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', () => {
|
describe('transactionLinkCode', () => {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
|
|
||||||
it('returns a string of length 24', () => {
|
it('returns a string of length 24', () => {
|
||||||
expect(transactionLinkCode(date)).toHaveLength(24)
|
expect(transactionLinkCode(date)).toHaveLength(24)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a string that ends with the hex value of date', () => {
|
it('returns a string that ends with the hex value of date', () => {
|
||||||
const regexp = new RegExp(date.getTime().toString(16) + '$')
|
const regexp = new RegExp(date.getTime().toString(16) + '$')
|
||||||
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
|
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -170,148 +170,154 @@ export class TransactionLinkResolver {
|
|||||||
if (code.match(/^CL-/)) {
|
if (code.match(/^CL-/)) {
|
||||||
// acquire lock
|
// acquire lock
|
||||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
logger.info('redeem contribution link...')
|
|
||||||
const now = new Date()
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
|
||||||
await queryRunner.connect()
|
|
||||||
await queryRunner.startTransaction('REPEATABLE READ')
|
|
||||||
try {
|
try {
|
||||||
const contributionLink = await queryRunner.manager
|
logger.info('redeem contribution link...')
|
||||||
.createQueryBuilder()
|
const now = new Date()
|
||||||
.select('contributionLink')
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
.from(DbContributionLink, 'contributionLink')
|
await queryRunner.connect()
|
||||||
.where('contributionLink.code = :code', { code: code.replace('CL-', '') })
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
.getOne()
|
try {
|
||||||
if (!contributionLink) {
|
const contributionLink = await queryRunner.manager
|
||||||
logger.error('no contribution link found to given code:', code)
|
.createQueryBuilder()
|
||||||
throw new Error('No contribution link found')
|
.select('contributionLink')
|
||||||
}
|
.from(DbContributionLink, 'contributionLink')
|
||||||
logger.info('...contribution link found with id', contributionLink.id)
|
.where('contributionLink.code = :code', { code: code.replace('CL-', '') })
|
||||||
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
|
.getOne()
|
||||||
logger.error(
|
if (!contributionLink) {
|
||||||
'contribution link is not valid yet. Valid from: ',
|
logger.error('no contribution link found to given code:', code)
|
||||||
contributionLink.validFrom,
|
throw new Error(`No contribution link found to given code: ${code}`)
|
||||||
)
|
|
||||||
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('...contribution link found with id', contributionLink.id)
|
||||||
let alreadyRedeemed: DbContribution | undefined
|
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
|
||||||
switch (contributionLink.cycle) {
|
logger.error(
|
||||||
case ContributionCycleType.ONCE: {
|
'contribution link is not valid yet. Valid from: ',
|
||||||
alreadyRedeemed = await queryRunner.manager
|
contributionLink.validFrom,
|
||||||
.createQueryBuilder()
|
)
|
||||||
.select('contribution')
|
throw new Error('Contribution link not valid yet')
|
||||||
.from(DbContribution, 'contribution')
|
}
|
||||||
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
if (contributionLink.validTo) {
|
||||||
linkId: contributionLink.id,
|
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
|
||||||
id: user.id,
|
|
||||||
})
|
|
||||||
.getOne()
|
|
||||||
if (alreadyRedeemed) {
|
|
||||||
logger.error(
|
logger.error(
|
||||||
'contribution link with rule ONCE already redeemed by user with id',
|
'contribution link is no longer valid. Valid to: ',
|
||||||
user.id,
|
contributionLink.validTo,
|
||||||
)
|
)
|
||||||
throw new Error('Contribution link already redeemed')
|
throw new Error('Contribution link is no longer valid')
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
case ContributionCycleType.DAILY: {
|
let alreadyRedeemed: DbContribution | undefined
|
||||||
const start = new Date()
|
switch (contributionLink.cycle) {
|
||||||
start.setHours(0, 0, 0, 0)
|
case ContributionCycleType.ONCE: {
|
||||||
const end = new Date()
|
alreadyRedeemed = await queryRunner.manager
|
||||||
end.setHours(23, 59, 59, 999)
|
.createQueryBuilder()
|
||||||
alreadyRedeemed = await queryRunner.manager
|
.select('contribution')
|
||||||
.createQueryBuilder()
|
.from(DbContribution, 'contribution')
|
||||||
.select('contribution')
|
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
||||||
.from(DbContribution, 'contribution')
|
|
||||||
.where(
|
|
||||||
`contribution.contributionLinkId = :linkId AND contribution.userId = :id
|
|
||||||
AND Date(contribution.confirmedAt) BETWEEN :start AND :end`,
|
|
||||||
{
|
|
||||||
linkId: contributionLink.id,
|
linkId: contributionLink.id,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
start,
|
})
|
||||||
end,
|
.getOne()
|
||||||
},
|
if (alreadyRedeemed) {
|
||||||
)
|
logger.error(
|
||||||
.getOne()
|
'contribution link with rule ONCE already redeemed by user with id',
|
||||||
if (alreadyRedeemed) {
|
user.id,
|
||||||
logger.error(
|
)
|
||||||
'contribution link with rule DAILY already redeemed by user with id',
|
throw new Error('Contribution link already redeemed')
|
||||||
user.id,
|
}
|
||||||
)
|
break
|
||||||
throw new Error('Contribution link already redeemed today')
|
}
|
||||||
|
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)
|
const creations = await getUserCreation(user.id, clientTimezoneOffset)
|
||||||
throw new Error('Contribution link has unknown cycle')
|
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 {
|
} finally {
|
||||||
await queryRunner.release()
|
|
||||||
releaseLock()
|
releaseLock()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -45,29 +45,28 @@ export const executeTransaction = async (
|
|||||||
recipient: dbUser,
|
recipient: dbUser,
|
||||||
transactionLink?: dbTransactionLink | null,
|
transactionLink?: dbTransactionLink | null,
|
||||||
): Promise<boolean> => {
|
): 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
|
// acquire lock
|
||||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
|
|
||||||
try {
|
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
|
// validate amount
|
||||||
const receivedCallDate = new Date()
|
const receivedCallDate = new Date()
|
||||||
const sendBalance = await calculateBalance(
|
const sendBalance = await calculateBalance(
|
||||||
@ -187,10 +186,10 @@ export const executeTransaction = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
logger.info(`finished executeTransaction successfully`)
|
logger.info(`finished executeTransaction successfully`)
|
||||||
return true
|
|
||||||
} finally {
|
} finally {
|
||||||
releaseLock()
|
releaseLock()
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user