Merge branch 'master' into optin-valid-time

This commit is contained in:
Moriz Wahl 2022-03-29 16:05:56 +02:00
commit 0c5719140d
20 changed files with 500 additions and 20 deletions

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
import CreationTransactionList from './CreationTransactionList.vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -46,11 +46,11 @@ const propsData = {
fields: ['date', 'balance', 'name', 'memo', 'decay'],
}
describe('CreationTransactionListFormular', () => {
describe('CreationTransactionList', () => {
let wrapper
const Wrapper = () => {
return mount(CreationTransactionListFormular, { localVue, mocks, propsData })
return mount(CreationTransactionList, { localVue, mocks, propsData })
}
describe('mount', () => {

View File

@ -1,6 +1,6 @@
<template>
<div class="component-creation-transaction-list">
{{ $t('transactionlist.title') }}
<div class="h3">{{ $t('transactionlist.title') }}</div>
<b-table striped hover :fields="fields" :items="items"></b-table>
</div>
</template>

View File

@ -0,0 +1,129 @@
import { mount } from '@vue/test-utils'
import OpenCreationsTable from './OpenCreationsTable.vue'
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue({})
const apolloQueryMock = jest.fn().mockResolvedValue({})
const propsData = {
items: [
{
id: 4,
firstName: 'Bob',
lastName: 'der Baumeister',
email: 'bob@baumeister.de',
amount: 300,
memo: 'Aktives Grundeinkommen für Januar 2022',
date: '2022-01-01T00:00:00.000Z',
moderator: 1,
creation: [700, 1000, 1000],
__typename: 'PendingCreation',
},
{
id: 5,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
email: 'raeuber@hotzenplotz.de',
amount: 210,
memo: 'Aktives Grundeinkommen für Januar 2022',
date: '2022-01-01T00:00:00.000Z',
moderator: 1,
creation: [790, 1000, 1000],
__typename: 'PendingCreation',
},
{
id: 6,
firstName: 'Stephen',
lastName: 'Hawking',
email: 'stephen@hawking.uk',
amount: 330,
memo: 'Aktives Grundeinkommen für Januar 2022',
date: '2022-01-01T00:00:00.000Z',
moderator: 1,
creation: [670, 1000, 1000],
__typename: 'PendingCreation',
},
],
fields: [
{ key: 'bookmark', label: 'delete' },
{ key: 'email', label: 'e_mail' },
{ key: 'firstName', label: 'firstname' },
{ key: 'lastName', label: 'lastname' },
{
key: 'amount',
label: 'creation',
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: 'text' },
{
key: 'date',
label: 'date',
formatter: (value) => {
return value
},
},
{ key: 'moderator', label: 'moderator' },
{ key: 'edit_creation', label: 'edit' },
{ key: 'confirm', label: 'save' },
],
}
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$apollo: {
mutate: apolloMutateMock,
query: apolloQueryMock,
},
$store: {
state: {
moderator: {
id: 0,
name: 'test moderator',
},
},
},
}
describe('OpenCreationsTable', () => {
let wrapper
const Wrapper = () => {
return mount(OpenCreationsTable, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV element with the class .open-creations-table', () => {
expect(wrapper.find('div.open-creations-table').exists()).toBeTruthy()
})
it('has a table with three rows', () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(3)
})
it('find first button.bi-pencil-square for open EditCreationFormular ', () => {
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBeTruthy()
})
describe('show edit details', () => {
beforeEach(async () => {
await wrapper.findAll('tr').at(1).find('.bi-pencil-square').trigger('click')
})
it.skip('has a component element with name EditCreationFormular', () => {
expect(wrapper.findComponent({ name: 'EditCreationFormular' }).exists()).toBe(true)
})
it.skip('renders the component component-edit-creation-formular', () => {
expect(wrapper.find('div.component-edit-creation-formular').exists()).toBeTruthy()
})
})
})
})

View File

@ -1,5 +1,5 @@
<template>
<div class="component-open-creations-table">
<div class="open-creations-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<b-button

View File

@ -69,10 +69,9 @@
: ''
"
/>
<creation-transaction-list-formular
v-if="!row.item.deletedAt"
:userId="row.item.userId"
/>
<creation-transaction-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
<transaction-link-list :userId="row.item.userId" />
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
</b-card>
</template>
@ -82,7 +81,8 @@
<script>
import CreationFormular from '../CreationFormular.vue'
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
import CreationTransactionListFormular from '../CreationTransactionListFormular.vue'
import CreationTransactionList from '../CreationTransactionList.vue'
import TransactionLinkList from '../TransactionLinkList.vue'
import DeletedUserFormular from '../DeletedUserFormular.vue'
export default {
@ -90,7 +90,8 @@ export default {
components: {
CreationFormular,
ConfirmRegisterMailFormular,
CreationTransactionListFormular,
CreationTransactionList,
TransactionLinkList,
DeletedUserFormular,
},
props: {

View File

@ -0,0 +1,140 @@
import { mount } from '@vue/test-utils'
import TransactionLinkList from './TransactionLinkList.vue'
import { listTransactionLinksAdmin } from '../graphql/listTransactionLinksAdmin.js'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloQueryMock = jest.fn()
apolloQueryMock.mockResolvedValue({
data: {
listTransactionLinksAdmin: {
linkCount: 8,
linkList: [
{
amount: '19.99',
code: '62ef8236ace7217fbd066c5a',
createdAt: '2022-03-24T17:43:09.000Z',
deletedAt: null,
holdAvailableAmount: '20.51411720068412022949',
id: 36,
memo: 'Kein Trick, keine Zauberrei,\nbei Gradidio sei dabei!',
redeemedAt: null,
validUntil: '2022-04-07T17:43:09.000Z',
},
{
amount: '19.99',
code: '2b603f36521c617fbd066cef',
createdAt: '2022-03-24T17:43:09.000Z',
deletedAt: null,
holdAvailableAmount: '20.51411720068412022949',
id: 37,
memo: 'Kein Trick, keine Zauberrei,\nbei Gradidio sei dabei!',
redeemedAt: null,
validUntil: '2022-04-07T17:43:09.000Z',
},
{
amount: '19.99',
code: '0bb789b5bd5b717fbd066eb5',
createdAt: '2022-03-24T17:43:09.000Z',
deletedAt: '2022-03-24T17:43:09.000Z',
holdAvailableAmount: '20.51411720068412022949',
id: 40,
memo: 'Da habe ich mich wohl etwas übernommen.',
redeemedAt: '2022-04-07T14:43:09.000Z',
validUntil: '2022-04-07T17:43:09.000Z',
},
{
amount: '19.99',
code: '2d4a763e516b317fbd066a85',
createdAt: '2022-01-01T00:00:00.000Z',
deletedAt: null,
holdAvailableAmount: '20.51411720068412022949',
id: 33,
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
redeemedAt: null,
validUntil: '2022-01-15T00:00:00.000Z',
},
],
},
},
})
const propsData = {
userId: 42,
}
const mocks = {
$apollo: {
query: apolloQueryMock,
},
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
}
describe('TransactionLinkList', () => {
let wrapper
const Wrapper = () => {
return mount(TransactionLinkList, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
query: listTransactionLinksAdmin,
variables: {
currentPage: 1,
pageSize: 5,
userId: 42,
},
}),
)
})
it('has 4 items in the table', () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(4)
})
it('has pagination buttons', () => {
expect(wrapper.findComponent({ name: 'BPagination' }).exists()).toBe(true)
})
describe('next page', () => {
beforeAll(async () => {
jest.clearAllMocks()
await wrapper.findComponent({ name: 'BPagination' }).vm.$emit('input', 2)
})
it('calls the API again', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
query: listTransactionLinksAdmin,
variables: {
currentPage: 1,
pageSize: 5,
userId: 42,
},
}),
)
})
})
describe('server response with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'Oh no!' })
wrapper = Wrapper()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})

View File

@ -0,0 +1,105 @@
<template>
<div class="transaction-link-list">
<div v-if="items.length > 0">
<div class="h3">{{ $t('transactionlink.form_header') }}</div>
<b-table striped hover :fields="fields" :items="items"></b-table>
</div>
<b-pagination
pills
size="lg"
v-model="currentPage"
:per-page="perPage"
:total-rows="rows"
align="center"
></b-pagination>
</div>
</template>
<script>
import { listTransactionLinksAdmin } from '../graphql/listTransactionLinksAdmin.js'
export default {
name: 'TransactionLinkList',
props: {
userId: { type: Number, required: true },
},
data() {
return {
items: [],
rows: 0,
currentPage: 1,
perPage: 5,
}
},
methods: {
getListTransactionLinks() {
this.$apollo
.query({
query: listTransactionLinksAdmin,
variables: {
currentPage: this.currentPage,
pageSize: this.perPage,
userId: this.userId,
},
})
.then((result) => {
this.rows = result.data.listTransactionLinksAdmin.linkCount
this.items = result.data.listTransactionLinksAdmin.linkList
})
.catch((error) => {
this.toastError(error.message)
})
},
},
computed: {
fields() {
return [
{
key: 'createdAt',
label: this.$t('transactionlink.created'),
formatter: (value, key, item) => {
return this.$d(new Date(value))
},
},
{
key: 'amount',
label: this.$t('transactionlist.amount'),
formatter: (value, key, item) => {
return `${value} GDD`
},
},
{ key: 'memo', label: this.$t('transactionlist.memo') },
{
key: 'validUntil',
label: this.$t('transactionlink.valid_until'),
formatter: (value, key, item) => {
return this.$d(new Date(value))
},
},
{
key: 'status',
label: 'status',
formatter: (value, key, item) => {
// deleted
if (item.deletedAt) return this.$t('deleted') + ': ' + this.$d(new Date(item.deletedAt))
// redeemed
if (item.redeemedAt)
return this.$t('redeemed') + ': ' + this.$d(new Date(item.redeemedAt))
// expired
if (new Date() > new Date(item.validUntil))
return this.$t('expired') + ': ' + this.$d(new Date(item.validUntil))
// open
return this.$t('open')
},
},
]
},
},
created() {
this.getListTransactionLinks()
},
watch: {
currentPage() {
this.getListTransactionLinks()
},
},
}
</script>

View File

@ -0,0 +1,20 @@
import gql from 'graphql-tag'
export const listTransactionLinksAdmin = gql`
query ($currentPage: Int = 1, $pageSize: Int = 5, $userId: Int!) {
listTransactionLinksAdmin(currentPage: $currentPage, pageSize: $pageSize, userId: $userId) {
linkCount
linkList {
id
amount
holdAvailableAmount
memo
code
createdAt
validUntil
redeemedAt
deletedAt
}
}
}
`

View File

@ -27,6 +27,7 @@
"edit": "Bearbeiten",
"enabled": "aktiviert",
"error": "Fehler",
"expired": "abgelaufen",
"e_mail": "E-Mail",
"firstname": "Vorname",
"footer": {
@ -56,6 +57,7 @@
"user_search": "Nutzersuche"
},
"not_open_creations": "Keine offenen Schöpfungen",
"open": "offen",
"open_creations": "Offene Schöpfungen",
"overlay": {
"confirm": {
@ -66,6 +68,7 @@
"yes": "Ja, Schöpfung bestätigen und speichern!"
}
},
"redeemed": "eingelöst",
"remove": "Entfernen",
"removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.",
"remove_all": "alle Nutzer entfernen",
@ -73,6 +76,11 @@
"status": "Status",
"success": "Erfolg",
"text": "Text",
"transactionlink": {
"created": "Erstellt",
"form_header": "Transaktion-Links",
"valid_until": "Gültig bis"
},
"transactionlist": {
"amount": "Betrag",
"balanceDate": "Schöpfungsdatum",

View File

@ -27,6 +27,7 @@
"edit": "Edit",
"enabled": "enabled",
"error": "Error",
"expired": "expired",
"e_mail": "E-mail",
"firstname": "Firstname",
"footer": {
@ -56,6 +57,7 @@
"user_search": "User search"
},
"not_open_creations": "No open creations",
"open": "open",
"open_creations": "Open creations",
"overlay": {
"confirm": {
@ -66,6 +68,7 @@
"yes": "Yes, confirm and save creation!"
}
},
"redeemed": "redeemed",
"remove": "Remove",
"removeNotSelf": "As admin / moderator you cannot delete yourself.",
"remove_all": "Remove all users",
@ -73,6 +76,11 @@
"status": "Status",
"success": "Success",
"text": "Text",
"transactionlink": {
"created": "Created",
"form_header": "Transaction links",
"valid_until": "Valid until"
},
"transactionlist": {
"amount": "Amount",
"balanceDate": "Creation date",

View File

@ -35,4 +35,5 @@ export enum RIGHTS {
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
}

View File

@ -0,0 +1,13 @@
import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export default class TransactionLinkFilters {
@Field(() => Boolean, { nullable: true, defaultValue: true })
withDeleted?: boolean
@Field(() => Boolean, { nullable: true, defaultValue: true })
withExpired?: boolean
@Field(() => Boolean, { nullable: true, defaultValue: true })
withRedeemed?: boolean
}

View File

@ -1,4 +1,4 @@
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Int } from 'type-graphql'
import Decimal from 'decimal.js-light'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { User } from './User'
@ -52,3 +52,12 @@ export class TransactionLink {
@Field(() => User, { nullable: true })
redeemedBy: User | null
}
@ObjectType()
export class TransactionLinkResult {
@Field(() => Int)
linkCount: number
@Field(() => [TransactionLink])
linkList: TransactionLink[]
}

View File

@ -9,6 +9,8 @@ import {
ObjectLiteral,
getConnection,
In,
MoreThan,
FindOperator,
} from '@dbTools/typeorm'
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
import { PendingCreation } from '@model/PendingCreation'
@ -21,6 +23,8 @@ import UpdatePendingCreationArgs from '@arg/UpdatePendingCreationArgs'
import SearchUsersArgs from '@arg/SearchUsersArgs'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { Transaction } from '@model/Transaction'
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { TransactionRepository } from '@repository/Transaction'
import { calculateDecay } from '@/util/decay'
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
@ -32,6 +36,7 @@ import { TransactionTypeId } from '@enum/TransactionTypeId'
import Decimal from 'decimal.js-light'
import { Decay } from '@model/Decay'
import Paginated from '@arg/Paginated'
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
import { Order } from '@enum/Order'
import { communityUser } from '@/util/communityUser'
import { checkOptInCode, activationLink } from './UserResolver'
@ -404,6 +409,41 @@ export class AdminResolver {
return true
}
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS_ADMIN])
@Query(() => TransactionLinkResult)
async listTransactionLinksAdmin(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
@Args()
filters: TransactionLinkFilters,
@Arg('userId', () => Int) userId: number,
): Promise<TransactionLinkResult> {
const user = await dbUser.findOneOrFail({ id: userId })
const where: {
userId: number
redeemedBy?: number | null
validUntil?: FindOperator<Date> | null
} = {
userId,
}
if (!filters.withRedeemed) where.redeemedBy = null
if (!filters.withExpired) where.validUntil = MoreThan(new Date())
const [transactionLinks, count] = await dbTransactionLink.findAndCount({
where,
withDeleted: filters.withDeleted,
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return {
linkCount: count,
linkList: transactionLinks.map((tl) => new TransactionLink(tl, new User(user))),
}
}
}
interface CreationMap {

View File

@ -49,6 +49,8 @@ KLICKTIPP_APIKEY_EN=
DATABASE_CONFIG_VERSION=v1.2022-03-18
# frontend
FRONTEND_CONFIG_VERSION=v1.2022-03-18
GRAPHQL_URI=https://stage1.gradido.net/graphql
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
@ -62,5 +64,7 @@ META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Econo
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
# admin
ADMIN_CONFIG_VERSION=v1.2022-03-18
WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token}
WALLET_URL=https://stage1.gradido.net/login

View File

@ -78,8 +78,8 @@
> git clone https://github.com/gradido/gradido.git
# Timezone
# Note: This is not needed - UTC(default) is REQUIRED for production data
# > sudo timedatectl set-timezone UTC
# Note: This is needed - since there is Summer-Time included in the default server Setup - UTC is REQUIRED for production data
> sudo timedatectl set-timezone UTC
# > sudo timedatectl set-ntp on
# > sudo apt purge ntp
# > sudo systemctl start systemd-timesyncd

View File

@ -67,7 +67,7 @@ BRANCH=${1:-master}
echo "Starting with git pull - branch:$BRANCH" >> $UPDATE_HTML
cd $PROJECT_ROOT
# TODO: this overfetches alot, but ensures we can use start.sh with tags
git fetch origin --all
git fetch --all
git checkout $BRANCH
git pull
export BUILD_COMMIT="$(git rev-parse HEAD)"

View File

@ -2,7 +2,9 @@
<div class="date-row">
<b-row>
<b-col cols="5">
<div class="text-right">{{ diffNow ? $t('gdd_per_link.expired') : $t('form.date') }}</div>
<div class="text-right">
{{ diffNow ? $t('gdd_per_link.valid_until') : $t('form.date') }}
</div>
</b-col>
<b-col cols="7">
<div class="gdd-transaction-list-item-date">

View File

@ -100,7 +100,6 @@
"decay-14-day": "Vergänglichkeit für 14 Tage",
"delete-the-link": "Den Link löschen?",
"deleted": "Der Link wurde gelöscht!",
"expired": "Abgelaufen",
"has-account": "Du besitzt bereits ein Gradido Konto",
"header": "Gradidos versenden per Link",
"link-copied": "Link wurde in die Zwischenablage kopiert",
@ -118,7 +117,8 @@
"redeemed-at": "Der Link wurde bereits am {date} eingelöst.",
"redeemed-title": "eingelöst",
"to-login": "Log dich ein",
"to-register": "Registriere ein neues Konto"
"to-register": "Registriere ein neues Konto",
"valid_until": "Gültig bis"
},
"gdt": {
"calculation": "Berechnung der GradidoTransform",

View File

@ -100,7 +100,6 @@
"decay-14-day": "Decay for 14 days",
"delete-the-link": "Delete the link?",
"deleted": "The link was deleted!",
"expired": "Expired",
"has-account": "You already have a Gradido account",
"header": "Send Gradidos via link",
"link-copied": "Link copied to clipboard",
@ -118,7 +117,8 @@
"redeemed-at": "The link was already redeemed on {date}.",
"redeemed-title": "redeemed",
"to-login": "Log in",
"to-register": "Register a new account"
"to-register": "Register a new account",
"valid_until": "Valid until"
},
"gdt": {
"calculation": "Calculation of GradidoTransform",