mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into test-login-user-resolver
This commit is contained in:
commit
103f727798
@ -36,6 +36,7 @@
|
||||
"graphql": "^15.6.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "26.6.3",
|
||||
"portal-vue": "^2.1.7",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"stats-webpack-plugin": "^0.7.0",
|
||||
"vue": "^2.6.11",
|
||||
@ -43,7 +44,6 @@
|
||||
"vue-i18n": "^8.26.5",
|
||||
"vue-jest": "^3.0.7",
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-toasted": "^1.1.28",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^4.1.0"
|
||||
},
|
||||
|
||||
@ -1,21 +1,17 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue'
|
||||
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue()
|
||||
const toastSuccessMock = jest.fn()
|
||||
const toastErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$toasted: {
|
||||
success: toastSuccessMock,
|
||||
error: toastErrorMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -54,7 +50,7 @@ describe('ConfirmRegisterMailFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessMock).toBeCalledWith('unregister_mail.success')
|
||||
expect(toastSuccessSpy).toBeCalledWith('unregister_mail.success')
|
||||
})
|
||||
})
|
||||
|
||||
@ -66,7 +62,7 @@ describe('ConfirmRegisterMailFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('unregister_mail.error')
|
||||
expect(toastErrorSpy).toBeCalledWith('unregister_mail.error')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -48,10 +48,10 @@ export default {
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$toasted.success(this.$t('unregister_mail.success', { email: this.email }))
|
||||
this.toastSuccess(this.$t('unregister_mail.success', { email: this.email }))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(this.$t('unregister_mail.error', { message: error.message }))
|
||||
this.toastError(this.$t('unregister_mail.error', { message: error.message }))
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import CreationFormular from './CreationFormular.vue'
|
||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
||||
import { createPendingCreations } from '../graphql/createPendingCreations'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -11,8 +12,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
},
|
||||
})
|
||||
const stateCommitMock = jest.fn()
|
||||
const toastedErrorMock = jest.fn()
|
||||
const toastedSuccessMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t, options) => (options ? [t, options] : t)),
|
||||
@ -32,10 +31,6 @@ const mocks = {
|
||||
},
|
||||
},
|
||||
},
|
||||
$toasted: {
|
||||
error: toastedErrorMock,
|
||||
success: toastedSuccessMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -140,7 +135,7 @@ describe('CreationFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith([
|
||||
expect(toastSuccessSpy).toBeCalledWith([
|
||||
'creation_form.toasted',
|
||||
{ email: 'benjamin@bluemchen.de', value: '90' },
|
||||
])
|
||||
@ -162,7 +157,7 @@ describe('CreationFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -292,7 +287,7 @@ describe('CreationFormular', () => {
|
||||
})
|
||||
|
||||
it('toast success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalled()
|
||||
expect(toastSuccessSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('store commit openCreationPlus', () => {
|
||||
@ -426,13 +421,14 @@ describe('CreationFormular', () => {
|
||||
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 0)
|
||||
})
|
||||
|
||||
it('toasts two errors', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith(
|
||||
'Could not created PendingCreation for bob@baumeister.de',
|
||||
)
|
||||
expect(toastedErrorMock).toBeCalledWith(
|
||||
'Could not created PendingCreation for bibi@bloxberg.de',
|
||||
)
|
||||
it('emits remove all bookmarks', () => {
|
||||
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('emits toast failed creations with two emails', () => {
|
||||
expect(wrapper.emitted('toast-failed-creations')).toEqual([
|
||||
[['bob@baumeister.de', 'bibi@bloxberg.de']],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -454,7 +450,7 @@ describe('CreationFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Oh no!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -166,20 +166,21 @@ export default {
|
||||
fetchPolicy: 'no-cache',
|
||||
})
|
||||
.then((result) => {
|
||||
const failedCreations = []
|
||||
this.$store.commit(
|
||||
'openCreationsPlus',
|
||||
result.data.createPendingCreations.successfulCreation.length,
|
||||
)
|
||||
if (result.data.createPendingCreations.failedCreation.length > 0) {
|
||||
result.data.createPendingCreations.failedCreation.forEach((failed) => {
|
||||
// TODO: Please localize this error message
|
||||
this.$toasted.error('Could not created PendingCreation for ' + failed)
|
||||
result.data.createPendingCreations.failedCreation.forEach((email) => {
|
||||
failedCreations.push(email)
|
||||
})
|
||||
}
|
||||
this.$emit('remove-all-bookmark')
|
||||
this.$emit('toast-failed-creations', failedCreations)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
} else if (this.type === 'singleCreation') {
|
||||
submitObj = {
|
||||
@ -196,19 +197,19 @@ export default {
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
|
||||
this.$toasted.success(
|
||||
this.$store.commit('openCreationsPlus', 1)
|
||||
this.toastSuccess(
|
||||
this.$t('creation_form.toasted', {
|
||||
value: this.value,
|
||||
email: this.item.email,
|
||||
}),
|
||||
)
|
||||
this.$store.commit('openCreationsPlus', 1)
|
||||
// what is this? Tests says that this.text is not reseted
|
||||
this.$refs.creationForm.reset()
|
||||
this.value = 0
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
this.$refs.creationForm.reset()
|
||||
this.value = 0
|
||||
})
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -50,17 +51,12 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
},
|
||||
})
|
||||
|
||||
const toastedErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$d: jest.fn((t) => t),
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastedErrorMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -109,7 +105,7 @@ describe('CreationTransactionListFormular', () => {
|
||||
})
|
||||
|
||||
it('toast error', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('OUCH!')
|
||||
expect(toastErrorSpy).toBeCalledWith('OUCH!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -62,7 +62,7 @@ export default {
|
||||
this.items = result.data.transactionList.transactions.filter((t) => t.type === 'creation')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import DeletedUserFormular from './DeletedUserFormular.vue'
|
||||
import { deleteUser } from '../graphql/deleteUser'
|
||||
import { unDeleteUser } from '../graphql/unDeleteUser'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -13,9 +14,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
},
|
||||
})
|
||||
|
||||
const toastedErrorMock = jest.fn()
|
||||
const toastedSuccessMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
@ -29,10 +27,6 @@ const mocks = {
|
||||
},
|
||||
},
|
||||
},
|
||||
$toasted: {
|
||||
error: toastedErrorMock,
|
||||
success: toastedSuccessMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -118,10 +112,6 @@ describe('DeletedUserFormular', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith('user_deleted')
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
@ -147,7 +137,7 @@ describe('DeletedUserFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Oh no!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -215,10 +205,6 @@ describe('DeletedUserFormular', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith('user_recovered')
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
@ -244,7 +230,7 @@ describe('DeletedUserFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Oh no!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -45,7 +45,6 @@ export default {
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$toasted.success(this.$t('user_deleted'))
|
||||
this.$emit('updateDeletedAt', {
|
||||
userId: this.item.userId,
|
||||
deletedAt: result.data.deleteUser,
|
||||
@ -53,7 +52,7 @@ export default {
|
||||
this.checked = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
unDeleteUser() {
|
||||
@ -65,7 +64,7 @@ export default {
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$toasted.success(this.$t('user_recovered'))
|
||||
this.toastSuccess(this.$t('user_recovered'))
|
||||
this.$emit('updateDeletedAt', {
|
||||
userId: this.item.userId,
|
||||
deletedAt: result.data.unDeleteUser,
|
||||
@ -73,7 +72,7 @@ export default {
|
||||
this.checked = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import EditCreationFormular from './EditCreationFormular.vue'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -16,8 +17,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
})
|
||||
|
||||
const stateCommitMock = jest.fn()
|
||||
const toastedErrorMock = jest.fn()
|
||||
const toastedSuccessMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
@ -37,10 +36,6 @@ const mocks = {
|
||||
},
|
||||
commit: stateCommitMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastedErrorMock,
|
||||
success: toastedSuccessMock,
|
||||
},
|
||||
}
|
||||
|
||||
const now = new Date(Date.now())
|
||||
@ -142,7 +137,7 @@ describe('EditCreationFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_update')
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_update')
|
||||
})
|
||||
})
|
||||
|
||||
@ -155,7 +150,7 @@ describe('EditCreationFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Oh no!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -132,7 +132,7 @@ export default {
|
||||
moderator: Number(result.data.updatePendingCreation.moderator),
|
||||
row: this.row,
|
||||
})
|
||||
this.$toasted.success(
|
||||
this.toastSuccess(
|
||||
this.$t('creation_form.toasted_update', {
|
||||
value: this.value,
|
||||
email: this.item.email,
|
||||
@ -144,7 +144,7 @@ export default {
|
||||
this.value = 0
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
// das creation Formular reseten
|
||||
this.$refs.updateCreationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
|
||||
@ -73,10 +73,6 @@ const mocks = {
|
||||
},
|
||||
},
|
||||
},
|
||||
$toasted: {
|
||||
error: jest.fn(),
|
||||
success: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
describe('SearchUserTable', () => {
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"confirmed": "bestätigt",
|
||||
"creation": "Schöpfung",
|
||||
"creation_form": {
|
||||
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
|
||||
"creation_for": "Aktives Grundeinkommen für",
|
||||
"enter_text": "Text eintragen",
|
||||
"form": "Schöpfungsformular",
|
||||
@ -28,6 +29,7 @@
|
||||
"delete_user": "Nutzer löschen",
|
||||
"details": "Details",
|
||||
"edit": "Bearbeiten",
|
||||
"error": "Fehler",
|
||||
"e_mail": "E-Mail",
|
||||
"firstname": "Vorname",
|
||||
"gradido_admin_footer": "Gradido Akademie Adminkonsole",
|
||||
@ -68,6 +70,7 @@
|
||||
"remove_all": "alle Nutzer entfernen",
|
||||
"save": "Speichern",
|
||||
"status": "Status",
|
||||
"success": "Erfolg",
|
||||
"text": "Text",
|
||||
"transaction": "Transaktion",
|
||||
"transactionlist": {
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"confirmed": "confirmed",
|
||||
"creation": "Creation",
|
||||
"creation_form": {
|
||||
"creation_failed": "Could not create pending creation for {email}",
|
||||
"creation_for": "Active Basic Income for",
|
||||
"enter_text": "Enter text",
|
||||
"form": "Creation form",
|
||||
@ -28,6 +29,7 @@
|
||||
"delete_user": "Delete user",
|
||||
"details": "Details",
|
||||
"edit": "Edit",
|
||||
"error": "Error",
|
||||
"e_mail": "E-mail",
|
||||
"firstname": "Firstname",
|
||||
"gradido_admin_footer": "Gradido Academy Admin Console",
|
||||
@ -68,6 +70,7 @@
|
||||
"remove_all": "Remove all users",
|
||||
"save": "Speichern",
|
||||
"status": "Status",
|
||||
"success": "Success",
|
||||
"text": "Text",
|
||||
"transaction": "Transaction",
|
||||
"transactionlist": {
|
||||
|
||||
@ -13,31 +13,24 @@ import i18n from './i18n'
|
||||
|
||||
import VueApollo from 'vue-apollo'
|
||||
|
||||
import PortalVue from 'portal-vue'
|
||||
|
||||
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import Toasted from 'vue-toasted'
|
||||
import { toasters } from './mixins/toaster'
|
||||
|
||||
import { apolloProvider } from './plugins/apolloProvider'
|
||||
|
||||
Vue.use(PortalVue)
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
Vue.use(IconsPlugin)
|
||||
|
||||
Vue.use(VueApollo)
|
||||
|
||||
Vue.use(Toasted, {
|
||||
position: 'top-center',
|
||||
duration: 5000,
|
||||
fullWidth: true,
|
||||
action: {
|
||||
text: 'x',
|
||||
onClick: (e, toastObject) => {
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
})
|
||||
Vue.mixin(toasters)
|
||||
|
||||
addNavigationGuards(router, store, apolloProvider.defaultClient, i18n)
|
||||
|
||||
|
||||
30
admin/src/mixins/toaster.js
Normal file
30
admin/src/mixins/toaster.js
Normal file
@ -0,0 +1,30 @@
|
||||
export const toasters = {
|
||||
methods: {
|
||||
toastSuccess(message) {
|
||||
this.toast(message, {
|
||||
title: this.$t('success'),
|
||||
variant: 'success',
|
||||
})
|
||||
},
|
||||
toastError(message) {
|
||||
this.toast(message, {
|
||||
title: this.$t('error'),
|
||||
variant: 'danger',
|
||||
})
|
||||
},
|
||||
toast(message, options) {
|
||||
// for unit tests, check that replace is present
|
||||
if (message.replace) message = message.replace(/^GraphQL error: /, '')
|
||||
this.$bvToast.toast(message, {
|
||||
autoHideDelay: 5000,
|
||||
appendToast: true,
|
||||
solid: true,
|
||||
toaster: 'b-toaster-top-right',
|
||||
headerClass: 'gdd-toaster-title',
|
||||
bodyClass: 'gdd-toaster-body',
|
||||
toastClass: 'gdd-toaster',
|
||||
...options,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Creation from './Creation.vue'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -29,18 +30,14 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
},
|
||||
})
|
||||
|
||||
const toastErrorMock = jest.fn()
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$t: jest.fn((t, options) => (options ? [t, options] : t)),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastErrorMock,
|
||||
},
|
||||
$store: {
|
||||
commit: storeCommitMock,
|
||||
state: {
|
||||
@ -236,6 +233,25 @@ describe('Creation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('failed creations', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'CreationFormular' })
|
||||
.vm.$emit('toast-failed-creations', ['bibi@bloxberg.de', 'benjamin@bluemchen.de'])
|
||||
})
|
||||
|
||||
it('toasts two error messages', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith([
|
||||
'creation_form.creation_failed',
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
])
|
||||
expect(toastErrorSpy).toBeCalledWith([
|
||||
'creation_form.creation_failed',
|
||||
{ email: 'benjamin@bluemchen.de' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('watchers', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@ -298,7 +314,7 @@ describe('Creation', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('Ouch')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
:creation="creation"
|
||||
:items="itemsMassCreation"
|
||||
@remove-all-bookmark="removeAllBookmarks"
|
||||
@toast-failed-creations="toastFailedCreations"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
@ -118,7 +119,7 @@ export default {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
pushItem(selectedItem) {
|
||||
@ -144,6 +145,11 @@ export default {
|
||||
this.$store.commit('setUserSelectedInMassCreation', [])
|
||||
this.getUsers()
|
||||
},
|
||||
toastFailedCreations(failedCreations) {
|
||||
failedCreations.forEach((email) =>
|
||||
this.toastError(this.$t('creation_form.creation_failed', { email })),
|
||||
)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
Searchfields() {
|
||||
|
||||
@ -2,12 +2,11 @@ import { mount } from '@vue/test-utils'
|
||||
import CreationConfirm from './CreationConfirm.vue'
|
||||
import { deletePendingCreation } from '../graphql/deletePendingCreation'
|
||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
const toastedErrorMock = jest.fn()
|
||||
const toastedSuccessMock = jest.fn()
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
getPendingCreations: [
|
||||
@ -47,10 +46,6 @@ const mocks = {
|
||||
query: apolloQueryMock,
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastedErrorMock,
|
||||
success: toastedSuccessMock,
|
||||
},
|
||||
}
|
||||
|
||||
describe('CreationConfirm', () => {
|
||||
@ -101,7 +96,7 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_delete')
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
|
||||
})
|
||||
})
|
||||
|
||||
@ -112,7 +107,7 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouchhh!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -158,7 +153,7 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_created')
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_created')
|
||||
})
|
||||
|
||||
it('has 1 item left in the table', () => {
|
||||
@ -173,7 +168,7 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouchhh!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -189,7 +184,7 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
it('toast an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -43,10 +43,10 @@ export default {
|
||||
})
|
||||
.then((result) => {
|
||||
this.updatePendingCreations(item.id)
|
||||
this.$toasted.success(this.$t('creation_form.toasted_delete'))
|
||||
this.toastSuccess(this.$t('creation_form.toasted_delete'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
confirmCreation() {
|
||||
@ -60,11 +60,11 @@ export default {
|
||||
.then((result) => {
|
||||
this.overlay = false
|
||||
this.updatePendingCreations(this.item.id)
|
||||
this.$toasted.success(this.$t('creation_form.toasted_created'))
|
||||
this.toastSuccess(this.$t('creation_form.toasted_created'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.overlay = false
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
getPendingCreations() {
|
||||
@ -79,7 +79,7 @@ export default {
|
||||
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
updatePendingCreations(id) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserSearch from './UserSearch.vue'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -15,6 +16,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
emailChecked: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
@ -23,6 +25,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [1000, 1000, 1000],
|
||||
emailChecked: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
userId: 3,
|
||||
@ -31,6 +34,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
email: 'peter@lustig.de',
|
||||
creation: [0, 0, 0],
|
||||
emailChecked: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
userId: 4,
|
||||
@ -39,23 +43,19 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
email: 'new@user.ch',
|
||||
creation: [1000, 1000, 1000],
|
||||
emailChecked: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const toastErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => String(d)),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastErrorMock,
|
||||
},
|
||||
}
|
||||
|
||||
describe('UserSearch', () => {
|
||||
@ -187,6 +187,21 @@ describe('UserSearch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete user', () => {
|
||||
const now = new Date()
|
||||
beforeEach(async () => {
|
||||
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now)
|
||||
})
|
||||
|
||||
it('marks the user as deleted', () => {
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('user_deleted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('apollo returns error', () => {
|
||||
beforeEach(() => {
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
@ -196,7 +211,7 @@ describe('UserSearch', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('Ouch')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -94,11 +94,12 @@ export default {
|
||||
this.searchResult = result.data.searchUsers.userList
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
updateDeletedAt(userId, deletedAt) {
|
||||
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
|
||||
this.toastSuccess(this.$t('user_deleted'))
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@ -5,11 +5,18 @@ import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
// without this async calls are not working
|
||||
import 'regenerator-runtime'
|
||||
|
||||
import { toasters } from '../src/mixins/toaster'
|
||||
|
||||
export const toastErrorSpy = jest.spyOn(toasters.methods, 'toastError')
|
||||
export const toastSuccessSpy = jest.spyOn(toasters.methods, 'toastSuccess')
|
||||
|
||||
global.localVue = createLocalVue()
|
||||
|
||||
global.localVue.use(BootstrapVue)
|
||||
global.localVue.use(IconsPlugin)
|
||||
|
||||
global.localVue.mixin(toasters)
|
||||
|
||||
// throw errors for vue warnings to force the programmers to take care about warnings
|
||||
Vue.config.warnHandler = (w) => {
|
||||
throw new Error(w)
|
||||
|
||||
@ -12512,11 +12512,6 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue-toasted@^1.1.28:
|
||||
version "1.1.28"
|
||||
resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.28.tgz#dbabb83acc89f7a9e8765815e491d79f0dc65c26"
|
||||
integrity sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw==
|
||||
|
||||
vue@^2.6.11:
|
||||
version "2.6.14"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
"axios": "^0.21.1",
|
||||
"class-validator": "^0.13.1",
|
||||
"cors": "^2.8.5",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^15.5.1",
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
import Decimal from 'decimal.js-light'
|
||||
dotenv.config()
|
||||
|
||||
Decimal.set({
|
||||
precision: 25,
|
||||
rounding: Decimal.ROUND_HALF_UP,
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0027-clean_transaction_table',
|
||||
DB_VERSION: '0029-clean_transaction_table',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ArgsType()
|
||||
export default class TransactionSendArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Number)
|
||||
amount: number
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@ -4,6 +4,8 @@ export enum TransactionTypeId {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
// This is a virtual property, never occurring on the database
|
||||
DECAY = 4,
|
||||
}
|
||||
|
||||
registerEnumType(TransactionTypeId, {
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class Balance {
|
||||
constructor(json: any) {
|
||||
this.balance = Number(json.balance)
|
||||
this.decay = Number(json.decay)
|
||||
this.balance = json.balance
|
||||
this.decay = json.decay
|
||||
this.decayDate = json.decay_date
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
@Field(() => Decimal)
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
decay: number
|
||||
@Field(() => Decimal)
|
||||
decay: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
decayDate: string
|
||||
@Field(() => Date)
|
||||
decayDate: Date
|
||||
}
|
||||
|
||||
@ -1,33 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class Decay {
|
||||
constructor(json?: any) {
|
||||
if (json) {
|
||||
this.balance = Number(json.balance)
|
||||
this.decayStart = json.decay_start
|
||||
this.decayEnd = json.decay_end
|
||||
this.decayDuration = json.decay_duration
|
||||
this.decayStartBlock = json.decay_start_block
|
||||
}
|
||||
constructor(
|
||||
balance: Decimal,
|
||||
decay: Decimal,
|
||||
start: Date | null,
|
||||
end: Date | null,
|
||||
duration: number | null,
|
||||
) {
|
||||
this.balance = balance
|
||||
this.decay = decay
|
||||
this.start = start
|
||||
this.end = end
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
@Field(() => Decimal)
|
||||
balance: Decimal
|
||||
|
||||
// timestamp in seconds
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayStart: string
|
||||
@Field(() => Decimal)
|
||||
decay: Decimal
|
||||
|
||||
// timestamp in seconds
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayEnd: string
|
||||
@Field(() => Date, { nullable: true })
|
||||
start: Date | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
decayDuration?: number
|
||||
@Field(() => Date, { nullable: true })
|
||||
end: Date | null
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayStartBlock?: string
|
||||
duration: number | null
|
||||
}
|
||||
|
||||
@ -1,59 +1,70 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { Decay } from './Decay'
|
||||
|
||||
// we need a better solution for the decay block:
|
||||
// the first transaction on the first page shows the decay since the last transaction
|
||||
// the format is actually a Decay and not a Transaction.
|
||||
// Therefore we have a lot of nullable fields, which should be always present
|
||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { User } from './User'
|
||||
|
||||
@ObjectType()
|
||||
export class Transaction {
|
||||
constructor() {
|
||||
this.type = ''
|
||||
this.balance = 0
|
||||
this.totalBalance = 0
|
||||
this.memo = ''
|
||||
this.firstTransaction = false
|
||||
constructor(transaction: dbTransaction, user: User, linkedUser: User | null = null) {
|
||||
this.id = transaction.id
|
||||
this.user = user
|
||||
this.previous = transaction.previous
|
||||
this.typeId = transaction.typeId
|
||||
this.amount = transaction.amount
|
||||
this.balance = transaction.balance
|
||||
this.balanceDate = transaction.balanceDate
|
||||
if (!transaction.decayStart) {
|
||||
this.decay = new Decay(transaction.balance, new Decimal(0), null, null, null)
|
||||
} else {
|
||||
this.decay = new Decay(
|
||||
transaction.balance,
|
||||
transaction.decay,
|
||||
transaction.decayStart,
|
||||
transaction.balanceDate,
|
||||
Math.round((transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000),
|
||||
)
|
||||
}
|
||||
this.memo = transaction.memo
|
||||
this.creationDate = transaction.creationDate
|
||||
this.linkedUser = linkedUser
|
||||
this.linkedTransactionId = transaction.linkedTransactionId
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
type: string
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
id: number
|
||||
|
||||
@Field(() => Number)
|
||||
totalBalance: number
|
||||
@Field(() => User)
|
||||
user: User
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayStart?: string
|
||||
@Field(() => Number, { nullable: true })
|
||||
previous: number | null
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayEnd?: string
|
||||
@Field(() => TransactionTypeId)
|
||||
typeId: TransactionTypeId
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayDuration?: number
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => Decimal)
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Date)
|
||||
balanceDate: Date
|
||||
|
||||
@Field(() => Decay)
|
||||
decay: Decay
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
creationDate: Date | null
|
||||
|
||||
@Field(() => User, { nullable: true })
|
||||
linkedUser: User | null
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
transactionId?: number
|
||||
|
||||
@Field({ nullable: true })
|
||||
name?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
email?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
date?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
decay?: Decay
|
||||
|
||||
@Field(() => Boolean)
|
||||
firstTransaction: boolean
|
||||
linkedTransactionId?: number | null
|
||||
}
|
||||
|
||||
@ -1,32 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import CONFIG from '../../config'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionList {
|
||||
constructor() {
|
||||
this.gdtSum = 0
|
||||
this.count = 0
|
||||
this.balance = 0
|
||||
this.decay = 0
|
||||
this.decayDate = ''
|
||||
constructor(
|
||||
balance: Decimal,
|
||||
transactions: Transaction[],
|
||||
count: number,
|
||||
balanceGDT?: number | null,
|
||||
decayStartBlock: Date = CONFIG.DECAY_START_TIME,
|
||||
) {
|
||||
this.balance = balance
|
||||
this.transactions = transactions
|
||||
this.count = count
|
||||
this.balanceGDT = balanceGDT || null
|
||||
this.decayStartBlock = decayStartBlock
|
||||
}
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
gdtSum: number | null
|
||||
balanceGDT: number | null
|
||||
|
||||
@Field(() => Number)
|
||||
count: number
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
decay: number
|
||||
|
||||
@Field(() => String)
|
||||
decayDate: string
|
||||
@Field(() => Date)
|
||||
decayStartBlock: Date
|
||||
|
||||
@Field(() => [Transaction])
|
||||
transactions: Transaction[]
|
||||
|
||||
@ -1,75 +1,74 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { KlickTipp } from './KlickTipp'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
|
||||
@ObjectType()
|
||||
export class User {
|
||||
/*
|
||||
@Field(() => ID)
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
*/
|
||||
constructor(json?: any) {
|
||||
if (json) {
|
||||
this.id = json.id
|
||||
this.email = json.email
|
||||
this.firstName = json.first_name
|
||||
this.lastName = json.last_name
|
||||
this.pubkey = json.public_hex
|
||||
this.language = json.language
|
||||
this.publisherId = json.publisher_id
|
||||
this.isAdmin = json.isAdmin
|
||||
}
|
||||
constructor(user: dbUser) {
|
||||
this.id = user.id
|
||||
this.email = user.email
|
||||
this.firstName = user.firstName
|
||||
this.lastName = user.lastName
|
||||
this.deletedAt = user.deletedAt
|
||||
this.createdAt = user.createdAt
|
||||
this.emailChecked = user.emailChecked
|
||||
this.language = user.language
|
||||
this.publisherId = user.publisherId
|
||||
// TODO
|
||||
this.isAdmin = null
|
||||
this.coinanimation = null
|
||||
this.klickTipp = null
|
||||
this.hasElopage = null
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
// `public_key` binary(32) DEFAULT NULL,
|
||||
// `privkey` binary(80) DEFAULT NULL,
|
||||
|
||||
// TODO privacy issue here
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
@Field(() => String, { nullable: true })
|
||||
firstName: string | null
|
||||
|
||||
@Field(() => String)
|
||||
lastName: string
|
||||
@Field(() => String, { nullable: true })
|
||||
lastName: string | null
|
||||
|
||||
@Field(() => String)
|
||||
pubkey: string
|
||||
/*
|
||||
@Field(() => String)
|
||||
pubkey: string
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
// not sure about the type here. Maybe better to have a string
|
||||
@Field(() => number)
|
||||
created: number
|
||||
// `password` bigint(20) unsigned DEFAULT 0,
|
||||
// `email_hash` binary(32) DEFAULT NULL,
|
||||
|
||||
@Field(() =>>> Boolean)
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => Boolean)
|
||||
emailChecked: boolean
|
||||
*/
|
||||
|
||||
@Field(() => String)
|
||||
language: string
|
||||
|
||||
/*
|
||||
@Field(() => Boolean)
|
||||
disabled: boolean
|
||||
*/
|
||||
// This is not the users publisherId, but the one of the users who recommend him
|
||||
@Field(() => Number, { nullable: true })
|
||||
publisherId: number | null
|
||||
|
||||
// what is publisherId?
|
||||
@Field(() => Int, { nullable: true })
|
||||
publisherId?: number
|
||||
// `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
|
||||
@Field(() => Boolean)
|
||||
isAdmin: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
coinanimation: boolean
|
||||
|
||||
@Field(() => KlickTipp)
|
||||
klickTipp: KlickTipp
|
||||
// TODO this is a bit inconsistent with what we query from the database
|
||||
// therefore all those fields are now nullable with default value null
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
isAdmin: boolean | null
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
hasElopage?: boolean
|
||||
coinanimation: boolean | null
|
||||
|
||||
@Field(() => KlickTipp, { nullable: true })
|
||||
klickTipp: KlickTipp | null
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
hasElopage: boolean | null
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ import { hasElopageBuys } from '../../util/hasElopageBuys'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { Balance } from '@entity/Balance'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||
@ -307,37 +307,30 @@ export class AdminResolver {
|
||||
const receivedCallDate = new Date()
|
||||
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const lastUserTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
|
||||
const lastTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
|
||||
|
||||
let newBalance = 0
|
||||
if (lastUserTransaction) {
|
||||
let newBalance = new Decimal(0)
|
||||
if (lastTransaction) {
|
||||
newBalance = calculateDecay(
|
||||
Number(lastUserTransaction.balance),
|
||||
lastUserTransaction.balanceDate,
|
||||
lastTransaction.balance,
|
||||
lastTransaction.balanceDate,
|
||||
receivedCallDate,
|
||||
).balance
|
||||
}
|
||||
newBalance = Number(newBalance) + Number(parseInt(pendingCreation.amount.toString()))
|
||||
// TODO pending creations decimal
|
||||
newBalance = newBalance.add(new Decimal(Number(pendingCreation.amount)))
|
||||
|
||||
const transaction = new Transaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = pendingCreation.memo
|
||||
transaction.userId = pendingCreation.userId
|
||||
transaction.amount = BigInt(parseInt(pendingCreation.amount.toString()))
|
||||
// TODO pending creations decimal
|
||||
transaction.amount = new Decimal(Number(pendingCreation.amount))
|
||||
transaction.creationDate = pendingCreation.date
|
||||
transaction.balance = BigInt(newBalance)
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = receivedCallDate
|
||||
await transaction.save()
|
||||
|
||||
let userBalance = await Balance.findOne({ userId: pendingCreation.userId })
|
||||
if (!userBalance) {
|
||||
userBalance = new Balance()
|
||||
userBalance.userId = pendingCreation.userId
|
||||
}
|
||||
userBalance.amount = Number(newBalance)
|
||||
userBalance.modified = receivedCallDate
|
||||
userBalance.recordDate = receivedCallDate
|
||||
await userBalance.save()
|
||||
await AdminPendingCreation.delete(pendingCreation)
|
||||
|
||||
return true
|
||||
|
||||
@ -6,9 +6,9 @@ import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import { Balance } from '../model/Balance'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { roundFloorFrom4 } from '../../util/round'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { Balance as dbBalance } from '@entity/Balance'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@Resolver()
|
||||
export class BalanceResolver {
|
||||
@ -18,24 +18,26 @@ export class BalanceResolver {
|
||||
// load user and balance
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const balanceEntity = await dbBalance.findOne({ userId: userEntity.id })
|
||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const now = new Date()
|
||||
|
||||
const lastTransaction = await Transaction.findOne(
|
||||
{ userId: user.id },
|
||||
{ order: { balanceDate: 'DESC' } },
|
||||
)
|
||||
|
||||
// No balance found
|
||||
if (!balanceEntity) {
|
||||
if (!lastTransaction) {
|
||||
return new Balance({
|
||||
balance: 0,
|
||||
decay: 0,
|
||||
balance: new Decimal(0),
|
||||
decay: new Decimal(0),
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
return new Balance({
|
||||
balance: roundFloorFrom4(balanceEntity.amount),
|
||||
decay: roundFloorFrom4(
|
||||
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
|
||||
),
|
||||
balance: lastTransaction.balance,
|
||||
decay: calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||
import { getCustomRepository, getConnection, QueryRunner } from '@dbTools/typeorm'
|
||||
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
||||
|
||||
import CONFIG from '../../config'
|
||||
import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail'
|
||||
@ -22,66 +22,17 @@ import { TransactionRepository } from '../../typeorm/repository/Transaction'
|
||||
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||
import { Balance as dbBalance } from '@entity/Balance'
|
||||
|
||||
import { apiPost } from '../../apis/HttpRequest'
|
||||
import { roundFloorFrom4, roundCeilFrom4 } from '../../util/round'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { TransactionType } from '../enum/TransactionType'
|
||||
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
|
||||
import { calculateBalance, isHexPublicKey } from '../../util/validate'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { User } from '../model/User'
|
||||
import { communityUser } from '../../util/communityUser'
|
||||
import { virtualDecayTransaction } from '../../util/virtualDecayTransaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
|
||||
// helper helper function
|
||||
async function updateStateBalance(
|
||||
user: dbUser,
|
||||
balance: number,
|
||||
received: Date,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<dbBalance> {
|
||||
let userBalance = await dbBalance.findOne({ userId: user.id })
|
||||
if (!userBalance) {
|
||||
userBalance = new dbBalance()
|
||||
userBalance.userId = user.id
|
||||
userBalance.amount = balance
|
||||
userBalance.modified = received
|
||||
} else {
|
||||
userBalance.amount = balance
|
||||
userBalance.modified = new Date()
|
||||
}
|
||||
if (userBalance.amount <= 0) {
|
||||
throw new Error('error new balance <= 0')
|
||||
}
|
||||
userBalance.recordDate = received
|
||||
return queryRunner.manager.save(userBalance).catch((error) => {
|
||||
throw new Error('error saving balance:' + error)
|
||||
})
|
||||
}
|
||||
|
||||
async function calculateNewBalance(
|
||||
userId: number,
|
||||
transactionDate: Date,
|
||||
centAmount: number,
|
||||
): Promise<number> {
|
||||
let newBalance = centAmount
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const lastUserTransaction = await transactionRepository.findLastForUser(userId)
|
||||
if (lastUserTransaction) {
|
||||
newBalance += Number(
|
||||
calculateDecay(
|
||||
Number(lastUserTransaction.balance),
|
||||
lastUserTransaction.balanceDate,
|
||||
transactionDate,
|
||||
).balance,
|
||||
)
|
||||
}
|
||||
|
||||
if (newBalance <= 0) {
|
||||
throw new Error('error new balance <= 0')
|
||||
}
|
||||
|
||||
return newBalance
|
||||
}
|
||||
@Resolver()
|
||||
export class TransactionResolver {
|
||||
@Authorized([RIGHTS.TRANSACTION_LIST])
|
||||
@ -97,165 +48,92 @@ export class TransactionResolver {
|
||||
}: Paginated,
|
||||
@Ctx() context: any,
|
||||
): Promise<TransactionList> {
|
||||
// load user
|
||||
const now = new Date()
|
||||
// find user
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
// TODO: separate those usecases - this is a security issue
|
||||
const user = userId
|
||||
? await userRepository.findOneOrFail({ id: userId }, { withDeleted: true })
|
||||
: await userRepository.findByPubkeyHex(context.pubKey)
|
||||
let limit = pageSize
|
||||
let offset = 0
|
||||
let skipFirstTransaction = false
|
||||
if (currentPage > 1) {
|
||||
offset = (currentPage - 1) * pageSize - 1
|
||||
limit++
|
||||
}
|
||||
if (offset && order === Order.ASC) {
|
||||
offset--
|
||||
}
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
|
||||
user.id,
|
||||
limit,
|
||||
offset,
|
||||
order,
|
||||
onlyCreations,
|
||||
|
||||
// find current balance
|
||||
const lastTransaction = await dbTransaction.findOne(
|
||||
{ userId: user.id },
|
||||
{ order: { balanceDate: 'DESC' } },
|
||||
)
|
||||
skipFirstTransaction = userTransactionsCount > offset + limit
|
||||
const decay = !(currentPage > 1)
|
||||
const transactions: Transaction[] = []
|
||||
if (userTransactions.length) {
|
||||
if (order === Order.DESC) {
|
||||
userTransactions.reverse()
|
||||
}
|
||||
const involvedUserIds: number[] = []
|
||||
|
||||
userTransactions.forEach((transaction: dbTransaction) => {
|
||||
involvedUserIds.push(transaction.userId)
|
||||
if (
|
||||
transaction.typeId === TransactionTypeId.SEND ||
|
||||
transaction.typeId === TransactionTypeId.RECEIVE
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
involvedUserIds.push(transaction.linkedUserId!) // TODO ensure not null properly
|
||||
}
|
||||
})
|
||||
// remove duplicates
|
||||
// https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates
|
||||
const involvedUsersUnique = involvedUserIds.filter((v, i, a) => a.indexOf(v) === i)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userIndiced = await userRepository.getUsersIndiced(involvedUsersUnique)
|
||||
|
||||
for (let i = 0; i < userTransactions.length; i++) {
|
||||
const userTransaction = userTransactions[i]
|
||||
const finalTransaction = new Transaction()
|
||||
finalTransaction.transactionId = userTransaction.id
|
||||
finalTransaction.date = userTransaction.balanceDate.toISOString()
|
||||
finalTransaction.memo = userTransaction.memo
|
||||
finalTransaction.totalBalance = roundFloorFrom4(Number(userTransaction.balance))
|
||||
const previousTransaction = i > 0 ? userTransactions[i - 1] : null
|
||||
|
||||
if (previousTransaction) {
|
||||
const currentTransaction = userTransaction
|
||||
const decay = calculateDecay(
|
||||
Number(previousTransaction.balance),
|
||||
previousTransaction.balanceDate,
|
||||
currentTransaction.balanceDate,
|
||||
)
|
||||
const balance = Number(previousTransaction.balance) - decay.balance
|
||||
|
||||
if (CONFIG.DECAY_START_TIME < currentTransaction.balanceDate) {
|
||||
finalTransaction.decay = decay
|
||||
finalTransaction.decay.balance = roundFloorFrom4(balance)
|
||||
if (
|
||||
previousTransaction.balanceDate < CONFIG.DECAY_START_TIME &&
|
||||
currentTransaction.balanceDate > CONFIG.DECAY_START_TIME
|
||||
) {
|
||||
finalTransaction.decay.decayStartBlock = (
|
||||
CONFIG.DECAY_START_TIME.getTime() / 1000
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalTransaction.balance = roundFloorFrom4(Number(userTransaction.amount)) // Todo unsafe conversion
|
||||
|
||||
const otherUser = userIndiced.find((u) => u.id === userTransaction.linkedUserId)
|
||||
switch (userTransaction.typeId) {
|
||||
case TransactionTypeId.CREATION:
|
||||
finalTransaction.name = 'Gradido Akademie'
|
||||
finalTransaction.type = TransactionType.CREATION
|
||||
break
|
||||
case TransactionTypeId.SEND:
|
||||
finalTransaction.type = TransactionType.SEND
|
||||
if (otherUser) {
|
||||
finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName
|
||||
finalTransaction.email = otherUser.email
|
||||
}
|
||||
break
|
||||
case TransactionTypeId.RECEIVE:
|
||||
finalTransaction.type = TransactionType.RECIEVE
|
||||
if (otherUser) {
|
||||
finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName
|
||||
finalTransaction.email = otherUser.email
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error('invalid transaction')
|
||||
}
|
||||
if (i > 0 || !skipFirstTransaction) {
|
||||
transactions.push(finalTransaction)
|
||||
}
|
||||
|
||||
if (i === userTransactions.length - 1 && decay) {
|
||||
const now = new Date()
|
||||
const decay = calculateDecay(
|
||||
Number(userTransaction.balance),
|
||||
userTransaction.balanceDate,
|
||||
now,
|
||||
)
|
||||
const balance = Number(userTransaction.balance) - decay.balance
|
||||
|
||||
const decayTransaction = new Transaction()
|
||||
decayTransaction.type = 'decay'
|
||||
decayTransaction.balance = roundCeilFrom4(balance)
|
||||
decayTransaction.decayDuration = decay.decayDuration
|
||||
decayTransaction.decayStart = decay.decayStart
|
||||
decayTransaction.decayEnd = decay.decayEnd
|
||||
transactions.push(decayTransaction)
|
||||
}
|
||||
}
|
||||
|
||||
if (order === Order.DESC) {
|
||||
transactions.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
const transactionList = new TransactionList()
|
||||
transactionList.count = userTransactionsCount
|
||||
transactionList.transactions = transactions
|
||||
|
||||
// get gdt sum
|
||||
transactionList.gdtSum = null
|
||||
// get GDT
|
||||
let balanceGDT = null
|
||||
try {
|
||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||
email: user.email,
|
||||
})
|
||||
if (resultGDTSum.success) transactionList.gdtSum = Number(resultGDTSum.data.sum) || 0
|
||||
} catch (err: any) {}
|
||||
|
||||
// get balance
|
||||
const balanceEntity = await dbBalance.findOne({ userId: user.id })
|
||||
if (balanceEntity) {
|
||||
const now = new Date()
|
||||
transactionList.balance = roundFloorFrom4(balanceEntity.amount)
|
||||
// TODO: Add a decay object here instead of static data representing the decay.
|
||||
transactionList.decay = roundFloorFrom4(
|
||||
calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now).balance,
|
||||
)
|
||||
transactionList.decayDate = now.toString()
|
||||
if (!resultGDTSum.success) {
|
||||
throw new Error('Call not successful')
|
||||
}
|
||||
balanceGDT = Number(resultGDTSum.data.sum) || 0
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Could not query GDT Server', err)
|
||||
}
|
||||
|
||||
return transactionList
|
||||
if (!lastTransaction) {
|
||||
return new TransactionList(new Decimal(0), [], 0, balanceGDT)
|
||||
}
|
||||
|
||||
// find transactions
|
||||
// first page can contain 26 due to virtual decay transaction
|
||||
const offset = (currentPage - 1) * pageSize
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
|
||||
user.id,
|
||||
pageSize,
|
||||
offset,
|
||||
order,
|
||||
onlyCreations,
|
||||
)
|
||||
|
||||
// find involved users; I am involved
|
||||
const involvedUserIds: number[] = [user.id]
|
||||
userTransactions.forEach((transaction: dbTransaction) => {
|
||||
if (transaction.linkedUserId && !involvedUserIds.includes(transaction.linkedUserId)) {
|
||||
involvedUserIds.push(transaction.linkedUserId)
|
||||
}
|
||||
})
|
||||
// We need to show the name for deleted users for old transactions
|
||||
const involvedDbUsers = await dbUser
|
||||
.createQueryBuilder()
|
||||
.withDeleted()
|
||||
.where('id IN (:...userIds)', { userIds: involvedUserIds })
|
||||
.getMany()
|
||||
const involvedUsers = involvedDbUsers.map((u) => new User(u))
|
||||
|
||||
const self = new User(user)
|
||||
const transactions: Transaction[] = []
|
||||
|
||||
// decay transaction
|
||||
if (currentPage === 1 && order === Order.DESC) {
|
||||
transactions.push(
|
||||
virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self),
|
||||
)
|
||||
}
|
||||
|
||||
// transactions
|
||||
userTransactions.forEach((userTransaction) => {
|
||||
const linkedUser =
|
||||
userTransaction.typeId === TransactionTypeId.CREATION
|
||||
? communityUser
|
||||
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
|
||||
transactions.push(new Transaction(userTransaction, self, linkedUser))
|
||||
})
|
||||
|
||||
// Construct Result
|
||||
return new TransactionList(
|
||||
calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
|
||||
transactions,
|
||||
userTransactionsCount,
|
||||
balanceGDT,
|
||||
)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEND_COINS])
|
||||
@ -271,7 +149,9 @@ export class TransactionResolver {
|
||||
throw new Error('invalid sender public key')
|
||||
}
|
||||
// validate amount
|
||||
if (!hasUserAmount(senderUser, amount)) {
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(senderUser.id, amount.mul(-1), receivedCallDate)
|
||||
if (!sendBalance) {
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
|
||||
@ -287,24 +167,22 @@ export class TransactionResolver {
|
||||
throw new Error('invalid recipient public key')
|
||||
}
|
||||
|
||||
const centAmount = Math.round(amount * 10000)
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
try {
|
||||
const receivedCallDate = new Date()
|
||||
// transaction
|
||||
const transactionSend = new dbTransaction()
|
||||
transactionSend.typeId = TransactionTypeId.SEND
|
||||
transactionSend.memo = memo
|
||||
transactionSend.userId = senderUser.id
|
||||
transactionSend.linkedUserId = recipientUser.id
|
||||
transactionSend.amount = BigInt(centAmount)
|
||||
const sendBalance = await calculateNewBalance(senderUser.id, receivedCallDate, -centAmount)
|
||||
transactionSend.balance = BigInt(Math.trunc(sendBalance))
|
||||
transactionSend.amount = amount.mul(-1)
|
||||
transactionSend.balance = sendBalance.balance
|
||||
transactionSend.balanceDate = receivedCallDate
|
||||
transactionSend.sendSenderFinalBalance = transactionSend.balance
|
||||
transactionSend.decay = sendBalance.decay.decay
|
||||
transactionSend.decayStart = sendBalance.decay.start
|
||||
transactionSend.previous = sendBalance.lastTransactionId
|
||||
await queryRunner.manager.insert(dbTransaction, transactionSend)
|
||||
|
||||
const transactionReceive = new dbTransaction()
|
||||
@ -312,15 +190,16 @@ export class TransactionResolver {
|
||||
transactionReceive.memo = memo
|
||||
transactionReceive.userId = recipientUser.id
|
||||
transactionReceive.linkedUserId = senderUser.id
|
||||
transactionReceive.amount = BigInt(centAmount)
|
||||
const receiveBalance = await calculateNewBalance(
|
||||
recipientUser.id,
|
||||
receivedCallDate,
|
||||
centAmount,
|
||||
)
|
||||
transactionReceive.balance = BigInt(Math.trunc(receiveBalance))
|
||||
transactionReceive.amount = amount
|
||||
const receiveBalance = await calculateBalance(recipientUser.id, amount, receivedCallDate)
|
||||
if (!receiveBalance) {
|
||||
throw new Error('Sender user account corrupted')
|
||||
}
|
||||
transactionReceive.balance = receiveBalance.balance
|
||||
transactionReceive.balanceDate = receivedCallDate
|
||||
transactionReceive.sendSenderFinalBalance = transactionSend.balance
|
||||
transactionReceive.decay = receiveBalance.decay.decay
|
||||
transactionReceive.decayStart = receiveBalance.decay.start
|
||||
transactionReceive.previous = receiveBalance.lastTransactionId
|
||||
transactionReceive.linkedTransactionId = transactionSend.id
|
||||
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
||||
|
||||
@ -328,17 +207,6 @@ export class TransactionResolver {
|
||||
transactionSend.linkedTransactionId = transactionReceive.id
|
||||
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
||||
|
||||
// Update Balance sender
|
||||
await updateStateBalance(senderUser, Math.trunc(sendBalance), receivedCallDate, queryRunner)
|
||||
|
||||
// Update Balance recipient
|
||||
await updateStateBalance(
|
||||
recipientUser,
|
||||
Math.trunc(receiveBalance),
|
||||
receivedCallDate,
|
||||
queryRunner,
|
||||
)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
@ -346,6 +214,7 @@ export class TransactionResolver {
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
|
||||
// send notification email
|
||||
// TODO: translate
|
||||
await sendTransactionReceivedEmail({
|
||||
|
||||
@ -216,14 +216,8 @@ export class UserResolver {
|
||||
// TODO refactor and do not have duplicate code with login(see below)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const user = new User()
|
||||
user.id = userEntity.id
|
||||
user.email = userEntity.email
|
||||
user.firstName = userEntity.firstName
|
||||
user.lastName = userEntity.lastName
|
||||
user.pubkey = userEntity.pubKey.toString('hex')
|
||||
user.language = userEntity.language
|
||||
|
||||
const user = new User(userEntity)
|
||||
// user.pubkey = userEntity.pubKey.toString('hex')
|
||||
// Elopage Status & Stored PublisherId
|
||||
user.hasElopage = await this.hasElopage(context)
|
||||
|
||||
@ -271,12 +265,9 @@ export class UserResolver {
|
||||
throw new Error('No user with this credentials')
|
||||
}
|
||||
|
||||
const user = new User()
|
||||
user.id = dbUser.id
|
||||
user.email = email
|
||||
user.firstName = dbUser.firstName
|
||||
user.lastName = dbUser.lastName
|
||||
user.pubkey = dbUser.pubKey.toString('hex')
|
||||
const user = new User(dbUser)
|
||||
// user.email = email
|
||||
// user.pubkey = dbUser.pubKey.toString('hex')
|
||||
user.language = dbUser.language
|
||||
|
||||
// Elopage Status & Stored PublisherId
|
||||
|
||||
23
backend/src/graphql/scalar/Decimal.ts
Normal file
23
backend/src/graphql/scalar/Decimal.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { GraphQLScalarType, Kind } from 'graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export default new GraphQLScalarType({
|
||||
name: 'Decimal',
|
||||
description: 'The `Decimal` scalar type to represent currency values',
|
||||
|
||||
serialize(value: Decimal) {
|
||||
return value.toString()
|
||||
},
|
||||
|
||||
parseValue(value) {
|
||||
return new Decimal(value)
|
||||
},
|
||||
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new TypeError(`${String(ast)} is not a valid decimal value.`)
|
||||
}
|
||||
|
||||
return new Decimal(ast.value)
|
||||
},
|
||||
})
|
||||
@ -3,11 +3,14 @@ import { buildSchema } from 'type-graphql'
|
||||
import path from 'path'
|
||||
|
||||
import isAuthorized from './directive/isAuthorized'
|
||||
import DecimalScalar from './scalar/Decimal'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
const schema = async (): Promise<GraphQLSchema> => {
|
||||
return buildSchema({
|
||||
resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)],
|
||||
authChecker: isAuthorized,
|
||||
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
@ -16,7 +17,7 @@ describe('sendTransactionReceivedEmail', () => {
|
||||
recipientFirstName: 'Peter',
|
||||
recipientLastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
amount: 42.0,
|
||||
amount: new Decimal(42.0),
|
||||
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { transactionReceived } from './text/transactionReceived'
|
||||
|
||||
@ -7,7 +8,7 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
email: string
|
||||
amount: number
|
||||
amount: Decimal
|
||||
memo: string
|
||||
}): Promise<boolean> => {
|
||||
return sendEMail({
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const transactionReceived = {
|
||||
de: {
|
||||
subject: 'Gradido Überweisung',
|
||||
@ -7,7 +9,7 @@ export const transactionReceived = {
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
email: string
|
||||
amount: number
|
||||
amount: Decimal
|
||||
memo: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||
|
||||
@ -9,14 +9,6 @@ export class UserRepository extends Repository<User> {
|
||||
.getOneOrFail()
|
||||
}
|
||||
|
||||
async getUsersIndiced(userIds: number[]): Promise<User[]> {
|
||||
return this.createQueryBuilder('user')
|
||||
.withDeleted() // We need to show the name for deleted users for old transactions
|
||||
.select(['user.id', 'user.firstName', 'user.lastName', 'user.email'])
|
||||
.where('user.id IN (:...userIds)', { userIds })
|
||||
.getMany()
|
||||
}
|
||||
|
||||
async findBySearchCriteriaPagedFiltered(
|
||||
select: string[],
|
||||
searchCriteria: string,
|
||||
|
||||
44
backend/src/util/communityUser.ts
Normal file
44
backend/src/util/communityUser.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { User } from '../graphql/model/User'
|
||||
|
||||
const communityDbUser: dbUser = {
|
||||
id: -1,
|
||||
email: 'support@gradido.net',
|
||||
firstName: 'Gradido',
|
||||
lastName: 'Akademie',
|
||||
pubKey: Buffer.from(''),
|
||||
privKey: Buffer.from(''),
|
||||
deletedAt: null,
|
||||
password: BigInt(0),
|
||||
emailHash: Buffer.from(''),
|
||||
createdAt: new Date(),
|
||||
emailChecked: false,
|
||||
language: '',
|
||||
publisherId: 0,
|
||||
passphrase: '',
|
||||
settings: [],
|
||||
hasId: function (): boolean {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
save: function (options?: SaveOptions): Promise<dbUser> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
remove: function (options?: RemoveOptions): Promise<dbUser> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
softRemove: function (options?: SaveOptions): Promise<dbUser> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
recover: function (options?: SaveOptions): Promise<dbUser> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
reload: function (): Promise<void> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
}
|
||||
const communityUser = new User(communityDbUser)
|
||||
|
||||
export { communityDbUser, communityUser }
|
||||
@ -1,41 +1,41 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import 'reflect-metadata' // This might be wise to load in a test setup file
|
||||
import { decayFormula, calculateDecay } from './decay'
|
||||
|
||||
describe('utils/decay', () => {
|
||||
describe('decayFormula', () => {
|
||||
it('has base 0.99999997802044727', () => {
|
||||
const amount = 1.0
|
||||
const amount = new Decimal(1.0)
|
||||
const seconds = 1
|
||||
expect(decayFormula(amount, seconds)).toBe(0.99999997802044727)
|
||||
})
|
||||
// Not sure if the following skiped tests make sence!?
|
||||
it('has negative decay?', async () => {
|
||||
const amount = -1.0
|
||||
const seconds = 1
|
||||
expect(await decayFormula(amount, seconds)).toBe(-0.99999997802044727)
|
||||
// TODO: toString() was required, we could not compare two decimals
|
||||
expect(decayFormula(amount, seconds).toString()).toBe('0.999999978035040489732012')
|
||||
})
|
||||
it('has correct backward calculation', async () => {
|
||||
const amount = 1.0
|
||||
const amount = new Decimal(1.0)
|
||||
const seconds = -1
|
||||
expect(await decayFormula(amount, seconds)).toBe(1.0000000219795533)
|
||||
expect(decayFormula(amount, seconds).toString()).toBe('1.000000021964959992727444')
|
||||
})
|
||||
// not possible, nodejs hasn't enough accuracy
|
||||
it('has correct forward calculation', async () => {
|
||||
const amount = 1.0 / 0.99999997802044727
|
||||
// we get pretty close, but not exact here, skipping
|
||||
it.skip('has correct forward calculation', async () => {
|
||||
const amount = new Decimal(1.0).div(
|
||||
new Decimal('0.99999997803504048973201202316767079413460520837376'),
|
||||
)
|
||||
const seconds = 1
|
||||
expect(await decayFormula(amount, seconds)).toBe(1.0)
|
||||
expect(decayFormula(amount, seconds).toString()).toBe('1.0')
|
||||
})
|
||||
})
|
||||
it.skip('has base 0.99999997802044727', async () => {
|
||||
it('has base 0.99999997802044727', async () => {
|
||||
const now = new Date()
|
||||
now.setSeconds(1)
|
||||
const oneSecondAgo = new Date(now.getTime())
|
||||
oneSecondAgo.setSeconds(0)
|
||||
expect(await calculateDecay(1.0, oneSecondAgo, now)).toBe(0.99999997802044727)
|
||||
expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now).balance.toString()).toBe(
|
||||
'0.999999978035040489732012',
|
||||
)
|
||||
})
|
||||
|
||||
it('returns input amount when from and to is the same', async () => {
|
||||
const now = new Date()
|
||||
expect((await calculateDecay(100.0, now, now)).balance).toBe(100.0)
|
||||
expect(calculateDecay(new Decimal(100.0), now, now).balance.toString()).toBe('100')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,45 +1,57 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import CONFIG from '../config'
|
||||
import { Decay } from '../graphql/model/Decay'
|
||||
|
||||
function decayFormula(amount: number, seconds: number): number {
|
||||
return amount * Math.pow(0.99999997802044727, seconds) // This number represents 50% decay a year
|
||||
// TODO: externalize all those definitions and functions into an external decay library
|
||||
|
||||
function decayFormula(value: Decimal, seconds: number): Decimal {
|
||||
// TODO why do we need to convert this here to a stting to work properly?
|
||||
return value.mul(
|
||||
new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds).toString(),
|
||||
)
|
||||
}
|
||||
|
||||
function calculateDecay(amount: number, from: Date, to: Date): Decay {
|
||||
function calculateDecay(
|
||||
amount: Decimal,
|
||||
from: Date,
|
||||
to: Date,
|
||||
startBlock: Date = CONFIG.DECAY_START_TIME,
|
||||
): Decay {
|
||||
const fromMs = from.getTime()
|
||||
const toMs = to.getTime()
|
||||
const decayStartBlockMs = CONFIG.DECAY_START_TIME.getTime()
|
||||
const startBlockMs = startBlock.getTime()
|
||||
|
||||
if (toMs < fromMs) {
|
||||
throw new Error('to < from, reverse decay calculation is invalid')
|
||||
}
|
||||
|
||||
// Initialize with no decay
|
||||
const decay = new Decay({
|
||||
const decay: Decay = {
|
||||
balance: amount,
|
||||
decayStart: null,
|
||||
decayEnd: null,
|
||||
decayDuration: 0,
|
||||
decayStartBlock: (decayStartBlockMs / 1000).toString(),
|
||||
})
|
||||
decay: new Decimal(0),
|
||||
start: null,
|
||||
end: null,
|
||||
duration: null,
|
||||
}
|
||||
|
||||
// decay started after end date; no decay
|
||||
if (decayStartBlockMs > toMs) {
|
||||
if (startBlockMs > toMs) {
|
||||
return decay
|
||||
}
|
||||
// decay started before start date; decay for full duration
|
||||
else if (decayStartBlockMs < fromMs) {
|
||||
decay.decayStart = (fromMs / 1000).toString()
|
||||
decay.decayDuration = (toMs - fromMs) / 1000
|
||||
if (startBlockMs < fromMs) {
|
||||
decay.start = from
|
||||
decay.duration = (toMs - fromMs) / 1000
|
||||
}
|
||||
// decay started between start and end date; decay from decay start till end date
|
||||
else {
|
||||
decay.decayStart = (decayStartBlockMs / 1000).toString()
|
||||
decay.decayDuration = (toMs - decayStartBlockMs) / 1000
|
||||
decay.start = startBlock
|
||||
decay.duration = (toMs - startBlockMs) / 1000
|
||||
}
|
||||
|
||||
decay.decayEnd = (toMs / 1000).toString()
|
||||
decay.balance = decayFormula(amount, decay.decayDuration)
|
||||
decay.end = to
|
||||
decay.balance = decayFormula(amount, decay.duration)
|
||||
decay.decay = decay.balance.minus(amount)
|
||||
return decay
|
||||
}
|
||||
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
import { roundCeilFrom4, roundFloorFrom4, roundCeilFrom2, roundFloorFrom2 } from './round'
|
||||
|
||||
describe('utils/round', () => {
|
||||
it('roundCeilFrom4', () => {
|
||||
const amount = 11617
|
||||
expect(roundCeilFrom4(amount)).toBe(1.17)
|
||||
})
|
||||
// Not sure if the following skiped tests make sence!?
|
||||
it('roundFloorFrom4', () => {
|
||||
const amount = 11617
|
||||
expect(roundFloorFrom4(amount)).toBe(1.16)
|
||||
})
|
||||
it('roundCeilFrom2', () => {
|
||||
const amount = 1216
|
||||
expect(roundCeilFrom2(amount)).toBe(13)
|
||||
})
|
||||
// not possible, nodejs hasn't enough accuracy
|
||||
it('roundFloorFrom2', () => {
|
||||
const amount = 1216
|
||||
expect(roundFloorFrom2(amount)).toBe(12)
|
||||
})
|
||||
})
|
||||
@ -1,17 +0,0 @@
|
||||
function roundCeilFrom4(decimal: number): number {
|
||||
return Math.ceil(decimal / 100) / 100
|
||||
}
|
||||
|
||||
function roundFloorFrom4(decimal: number): number {
|
||||
return Math.floor(decimal / 100) / 100
|
||||
}
|
||||
|
||||
function roundCeilFrom2(decimal: number): number {
|
||||
return Math.ceil(decimal / 100)
|
||||
}
|
||||
|
||||
function roundFloorFrom2(decimal: number): number {
|
||||
return Math.floor(decimal / 100)
|
||||
}
|
||||
|
||||
export { roundCeilFrom4, roundFloorFrom4, roundCeilFrom2, roundFloorFrom2 }
|
||||
@ -1,7 +1,7 @@
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { Balance as dbBalance } from '@entity/Balance'
|
||||
import { getRepository } from '@dbTools/typeorm'
|
||||
import { calculateDecay } from './decay'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Decay } from '../graphql/model/Decay'
|
||||
|
||||
function isStringBoolean(value: string): boolean {
|
||||
const lowerValue = value.toLowerCase()
|
||||
@ -15,14 +15,21 @@ function isHexPublicKey(publicKey: string): boolean {
|
||||
return /^[0-9A-Fa-f]{64}$/i.test(publicKey)
|
||||
}
|
||||
|
||||
async function hasUserAmount(user: dbUser, amount: number): Promise<boolean> {
|
||||
if (amount < 0) return false
|
||||
const balanceRepository = getRepository(dbBalance)
|
||||
const balance = await balanceRepository.findOne({ userId: user.id })
|
||||
if (!balance) return false
|
||||
async function calculateBalance(
|
||||
userId: number,
|
||||
amount: Decimal,
|
||||
time: Date,
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
||||
if (!lastTransaction) return null
|
||||
|
||||
const decay = calculateDecay(balance.amount, balance.recordDate, new Date()).balance
|
||||
return decay > amount
|
||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||
// TODO why we have to use toString() here?
|
||||
const balance = decay.balance.add(amount.toString())
|
||||
if (balance.lessThan(0)) {
|
||||
return null
|
||||
}
|
||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||
}
|
||||
|
||||
export { isHexPublicKey, hasUserAmount, isStringBoolean }
|
||||
export { isHexPublicKey, calculateBalance, isStringBoolean }
|
||||
|
||||
52
backend/src/util/virtualDecayTransaction.ts
Normal file
52
backend/src/util/virtualDecayTransaction.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||
import { calculateDecay } from './decay'
|
||||
import { TransactionTypeId } from '../graphql/enum/TransactionTypeId'
|
||||
import { Transaction } from '../graphql/model/Transaction'
|
||||
import { User } from '../graphql/model/User'
|
||||
|
||||
const virtualDecayTransaction = (
|
||||
balance: Decimal,
|
||||
balanceDate: Date,
|
||||
time: Date = new Date(),
|
||||
user: User,
|
||||
): Transaction => {
|
||||
const decay = calculateDecay(balance, balanceDate, time)
|
||||
// const balance = decay.balance.minus(lastTransaction.balance)
|
||||
const decayDbTransaction: dbTransaction = {
|
||||
id: -1,
|
||||
userId: -1,
|
||||
previous: -1,
|
||||
typeId: TransactionTypeId.DECAY,
|
||||
amount: decay.decay ? decay.decay : new Decimal(0), // new Decimal(0), // this kinda is wrong, but helps with the frontend query
|
||||
balance: decay.balance,
|
||||
balanceDate: time,
|
||||
decay: decay.decay ? decay.decay : new Decimal(0),
|
||||
decayStart: decay.start,
|
||||
memo: '',
|
||||
creationDate: null,
|
||||
hasId: function (): boolean {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
save: function (options?: SaveOptions): Promise<dbTransaction> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
remove: function (options?: RemoveOptions): Promise<dbTransaction> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
softRemove: function (options?: SaveOptions): Promise<dbTransaction> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
recover: function (options?: SaveOptions): Promise<dbTransaction> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
reload: function (): Promise<void> {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
}
|
||||
return new Transaction(decayDbTransaction, user)
|
||||
}
|
||||
|
||||
export { virtualDecayTransaction }
|
||||
@ -1961,6 +1961,11 @@ debug@^3.2.6, debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
decimal.js-light@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
|
||||
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
|
||||
|
||||
decimal.js@^10.2.1:
|
||||
version "10.3.1"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
|
||||
import { Balance } from '../Balance'
|
||||
import { Balance } from './Balance'
|
||||
|
||||
// Moriz: I do not like the idea of having two user tables
|
||||
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
|
||||
146
database/entity/0028-decimal_types/Transaction.ts
Normal file
146
database/entity/0028-decimal_types/Transaction.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
|
||||
@Entity('transactions')
|
||||
export class Transaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({ unsigned: true, nullable: true, default: null })
|
||||
previous: number
|
||||
|
||||
@Column({ name: 'type_id', unsigned: true, nullable: false })
|
||||
typeId: number
|
||||
|
||||
@Column({
|
||||
name: 'dec_amount',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
decAmount: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'dec_balance',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
decBalance: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'balance_date',
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
nullable: false,
|
||||
})
|
||||
balanceDate: Date
|
||||
|
||||
@Column({
|
||||
name: 'dec_decay',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
decDecay: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'decay_start',
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
decayStart: Date | null
|
||||
|
||||
@Column({ type: 'bigint', nullable: false })
|
||||
amount: BigInt
|
||||
|
||||
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
memo: string
|
||||
|
||||
@Column({
|
||||
name: 'send_sender_final_balance',
|
||||
type: 'bigint',
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
sendSenderFinalBalance: BigInt | null
|
||||
|
||||
@Column({ name: 'balance', type: 'bigint', default: 0 })
|
||||
balance: BigInt
|
||||
|
||||
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
|
||||
creationDate: Date
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
linkedUserId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_transaction_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
linkedTransactionId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'temp_dec_send_sender_final_balance',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
tempDecSendSenderFinalBalance: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'temp_dec_diff_send_sender_final_balance',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
tempDecDiffSendSenderFinalBalance: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'temp_dec_old_balance',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
tempDecOldBalance: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'temp_dec_diff_balance',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
tempDecDiffBalance: Decimal
|
||||
}
|
||||
85
database/entity/0029-clean_transaction_table/Transaction.ts
Normal file
85
database/entity/0029-clean_transaction_table/Transaction.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
|
||||
@Entity('transactions')
|
||||
export class Transaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({ unsigned: true, nullable: true, default: null })
|
||||
previous: number
|
||||
|
||||
@Column({ name: 'type_id', unsigned: true, nullable: false })
|
||||
typeId: number
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount: Decimal
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
balance: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'balance_date',
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
nullable: false,
|
||||
})
|
||||
balanceDate: Date
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
decay: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'decay_start',
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
decayStart: Date | null
|
||||
|
||||
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
memo: string
|
||||
|
||||
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
|
||||
creationDate: Date | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
linkedUserId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_transaction_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
linkedTransactionId?: number | null
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export { Balance } from './0001-init_db/Balance'
|
||||
@ -1 +1 @@
|
||||
export { Transaction } from './0027-clean_transaction_table/Transaction'
|
||||
export { Transaction } from './0029-clean_transaction_table/Transaction'
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { Balance } from './Balance'
|
||||
import { LoginElopageBuys } from './LoginElopageBuys'
|
||||
import { LoginEmailOptIn } from './LoginEmailOptIn'
|
||||
import { Migration } from './Migration'
|
||||
@ -10,7 +9,6 @@ import { AdminPendingCreation } from './AdminPendingCreation'
|
||||
|
||||
export const entities = [
|
||||
AdminPendingCreation,
|
||||
Balance,
|
||||
LoginElopageBuys,
|
||||
LoginEmailOptIn,
|
||||
Migration,
|
||||
|
||||
231
database/migrations/0028-decimal_types.ts
Normal file
231
database/migrations/0028-decimal_types.ts
Normal file
@ -0,0 +1,231 @@
|
||||
/* MIGRATION TO INTRODUCE THE DECIMAL TYPE
|
||||
*
|
||||
* This migration adds fields of type DECIMAL
|
||||
* and corrects the corresponding values of
|
||||
* each by recalculating the history of all
|
||||
* user transactions.
|
||||
*
|
||||
* Furthermore it increases precision of the
|
||||
* stored values and stores additional data
|
||||
* points to avoid repetitive calculations.
|
||||
*
|
||||
* It will also add a link to the last
|
||||
* transaction, creating a linked list
|
||||
*
|
||||
* And it will convert all timestamps to
|
||||
* datetime.
|
||||
*
|
||||
* WARNING: This Migration must be run in TZ=UTC
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
// Set precision value
|
||||
Decimal.set({
|
||||
precision: 25,
|
||||
rounding: Decimal.ROUND_HALF_UP,
|
||||
})
|
||||
|
||||
const DECAY_START_TIME = new Date('2021-05-13 17:46:31') // GMT+0
|
||||
|
||||
// TODO: externalize all those definitions and functions into an external decay library
|
||||
interface Decay {
|
||||
balance: Decimal
|
||||
decay: Decimal | null
|
||||
start: Date | null
|
||||
end: Date | null
|
||||
duration: number | null
|
||||
}
|
||||
|
||||
export enum TransactionTypeId {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
}
|
||||
|
||||
function decayFormula(value: Decimal, seconds: number): Decimal {
|
||||
return value.mul(new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds))
|
||||
}
|
||||
|
||||
function calculateDecay(
|
||||
amount: Decimal,
|
||||
from: Date,
|
||||
to: Date,
|
||||
startBlock: Date = DECAY_START_TIME,
|
||||
): Decay {
|
||||
const fromMs = from.getTime()
|
||||
const toMs = to.getTime()
|
||||
const startBlockMs = startBlock.getTime()
|
||||
|
||||
if (toMs < fromMs) {
|
||||
throw new Error('to < from, reverse decay calculation is invalid')
|
||||
}
|
||||
|
||||
// Initialize with no decay
|
||||
const decay: Decay = {
|
||||
balance: amount,
|
||||
decay: null,
|
||||
start: null,
|
||||
end: null,
|
||||
duration: null,
|
||||
}
|
||||
|
||||
// decay started after end date; no decay
|
||||
if (startBlockMs > toMs) {
|
||||
return decay
|
||||
}
|
||||
// decay started before start date; decay for full duration
|
||||
if (startBlockMs < fromMs) {
|
||||
decay.start = from
|
||||
decay.duration = (toMs - fromMs) / 1000
|
||||
}
|
||||
// decay started between start and end date; decay from decay start till end date
|
||||
else {
|
||||
decay.start = startBlock
|
||||
decay.duration = (toMs - startBlockMs) / 1000
|
||||
}
|
||||
|
||||
decay.end = to
|
||||
decay.balance = decayFormula(amount, decay.duration)
|
||||
decay.decay = decay.balance.minus(amount)
|
||||
return decay
|
||||
}
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// Add Columns
|
||||
|
||||
// add column `previous` for a link to the previous transaction
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `previous` int(10) unsigned DEFAULT NULL AFTER `user_id`;',
|
||||
)
|
||||
// add column `dec_amount` with temporary NULL and DEFAULT NULL definition
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `dec_amount` DECIMAL(40,20) NULL DEFAULT NULL AFTER `type_id`;',
|
||||
)
|
||||
// add column `dec_balance` with temporary NULL and DEFAULT NULL definition
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `dec_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `dec_amount`;',
|
||||
)
|
||||
// add new column `dec_decay` with temporary NULL and DEFAULT NULL definition
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `dec_decay` DECIMAL(40,20) NULL DEFAULT NULL AFTER `dec_balance`;',
|
||||
)
|
||||
// add new column `decay_start`
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `decay_start` datetime DEFAULT NULL AFTER `dec_decay`;',
|
||||
)
|
||||
|
||||
// Modify columns
|
||||
|
||||
// modify date type of `balance_date` to datetime
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` MODIFY COLUMN `balance_date` datetime NOT NULL DEFAULT current_timestamp() AFTER `dec_balance`;',
|
||||
)
|
||||
// modify date type of `creation_date` to datetime
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` MODIFY COLUMN `creation_date` datetime DEFAULT NULL AFTER `balance`;',
|
||||
)
|
||||
|
||||
// Temporary columns
|
||||
|
||||
// temporary decimal column `temp_dec_send_sender_final_balance`
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_send_sender_final_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `linked_transaction_id`;',
|
||||
)
|
||||
// temporary decimal column `temp_dec_diff_send_sender_final_balance`
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_diff_send_sender_final_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `temp_dec_send_sender_final_balance`;',
|
||||
)
|
||||
// temporary decimal column `temp_dec_old_balance`
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_old_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `temp_dec_diff_send_sender_final_balance`;',
|
||||
)
|
||||
// temporary decimal column `temp_dec_diff_balance`
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_diff_balance` DECIMAL(40,20) NULL DEFAULT NULL AFTER `temp_dec_old_balance`;',
|
||||
)
|
||||
|
||||
// Find all users & loop over them
|
||||
const users = await queryFn('SELECT user_id FROM transactions GROUP BY user_id')
|
||||
for (let u = 0; u < users.length; u++) {
|
||||
// find all transactions for a user
|
||||
const transactions = await queryFn(
|
||||
`SELECT * FROM transactions WHERE user_id = ${users[u].user_id} ORDER BY balance_date ASC;`,
|
||||
)
|
||||
let previous = null
|
||||
let balance = new Decimal(0)
|
||||
for (let t = 0; t < transactions.length; t++) {
|
||||
const transaction = transactions[t]
|
||||
|
||||
// This should also fix the rounding error on amount
|
||||
let decAmount = new Decimal(transaction.amount).dividedBy(10000).toDecimalPlaces(2)
|
||||
if (transaction.type_id === TransactionTypeId.SEND) {
|
||||
decAmount = decAmount.mul(-1)
|
||||
}
|
||||
const decayStartDate = previous ? previous.balance_date : transaction.balance_date
|
||||
const decay = calculateDecay(balance, decayStartDate, transaction.balance_date)
|
||||
// WARNING: `toISOString()` needs UTC Timezone to work properly!
|
||||
const decayStart =
|
||||
previous && decay.start
|
||||
? '"' + decay.start.toISOString().slice(0, 19).replace('T', ' ') + '"'
|
||||
: null
|
||||
balance = decay.balance.add(decAmount)
|
||||
const tempDecSendSenderFinalBalance = transaction.send_sender_final_balance
|
||||
? new Decimal(transaction.send_sender_final_balance).dividedBy(10000)
|
||||
: null
|
||||
const tempDecDiffSendSenderFinalBalance = tempDecSendSenderFinalBalance
|
||||
? balance.minus(tempDecSendSenderFinalBalance)
|
||||
: null
|
||||
const tempDecOldBalance = new Decimal(transaction.balance).dividedBy(10000)
|
||||
const tempDecDiffBalance = balance.minus(tempDecOldBalance)
|
||||
|
||||
// Update
|
||||
await queryFn(`
|
||||
UPDATE transactions SET
|
||||
previous = ${previous ? previous.id : null},
|
||||
dec_amount = ${decAmount.toString()},
|
||||
dec_balance = ${balance.toString()},
|
||||
dec_decay = ${decay.decay ? decay.decay.toString() : '0'},
|
||||
decay_start = ${decayStart},
|
||||
temp_dec_send_sender_final_balance = ${
|
||||
tempDecSendSenderFinalBalance ? tempDecSendSenderFinalBalance.toString() : null
|
||||
},
|
||||
temp_dec_diff_send_sender_final_balance = ${
|
||||
tempDecDiffSendSenderFinalBalance ? tempDecDiffSendSenderFinalBalance.toString() : null
|
||||
},
|
||||
temp_dec_old_balance = ${tempDecOldBalance.toString()},
|
||||
temp_dec_diff_balance = ${tempDecDiffBalance.toString()}
|
||||
WHERE id = ${transaction.id};
|
||||
`)
|
||||
|
||||
// previous
|
||||
previous = transaction
|
||||
}
|
||||
}
|
||||
|
||||
// Remove null as value & default value from `dec_amount`, `dec_balance` and `dec_decay`
|
||||
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `dec_amount` DECIMAL(40,20) NOT NULL;')
|
||||
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `dec_balance` DECIMAL(40,20) NOT NULL;')
|
||||
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `dec_decay` DECIMAL(40,20) NOT NULL;')
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_balance`')
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_old_balance`;')
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_send_sender_final_balance`;')
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_send_sender_final_balance`;')
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` MODIFY COLUMN `creation_date` timestamp NULL DEFAULT NULL AFTER `balance`;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` MODIFY COLUMN `balance_date` timestamp NOT NULL DEFAULT current_timestamp() AFTER `dec_balance`;',
|
||||
)
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `decay_start`;')
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `dec_decay`;')
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `dec_balance`;')
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `dec_amount`;')
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `previous`;')
|
||||
}
|
||||
105
database/migrations/0029-clean_transaction_table.ts
Normal file
105
database/migrations/0029-clean_transaction_table.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/* MIGRATION TO CLEAN THE TRANSACTION TABLE
|
||||
*
|
||||
* This migration deletes and renames several
|
||||
* columns of the `transactions` table.
|
||||
*
|
||||
* This migration has data loss
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// Delete columns
|
||||
|
||||
// delete column `amount`
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `amount`;')
|
||||
// delete column `send_sender_final_balance`
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `send_sender_final_balance`;')
|
||||
// delete column `balance`
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `balance`;')
|
||||
// delete column `temp_dec_send_sender_final_balance`
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_send_sender_final_balance`;')
|
||||
// delete column `temp_dec_diff_send_sender_final_balance`
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_send_sender_final_balance`;')
|
||||
// delete column `temp_dec_old_balance`
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_old_balance`;')
|
||||
// delete column `temp_dec_diff_balance`
|
||||
await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_balance`;')
|
||||
|
||||
// Rename columns
|
||||
|
||||
// rename column `dec_amount` to `amount`
|
||||
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `dec_amount` to `amount`;')
|
||||
|
||||
// rename column `dec_balance` to `balance`
|
||||
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `dec_balance` to `balance`;')
|
||||
|
||||
// rename column `dec_decay` to `decay`
|
||||
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `dec_decay` to `decay`;')
|
||||
|
||||
// Drop tables
|
||||
|
||||
// drop `state_balances`
|
||||
await queryFn('DROP TABLE `state_balances`;')
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// Not all data is recoverable here, this data is simulated,
|
||||
// We lose all incorrect balances and wrongly rounded amounts.
|
||||
|
||||
await queryFn(`
|
||||
CREATE TABLE \`state_balances\` (
|
||||
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`state_user_id\` int(10) unsigned NOT NULL,
|
||||
\`modified\` datetime NOT NULL,
|
||||
\`record_date\` datetime DEFAULT NULL,
|
||||
\`amount\` bigint(20) NOT NULL,
|
||||
PRIMARY KEY (\`id\`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=568 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`)
|
||||
await queryFn(`
|
||||
INSERT INTO \`state_balances\`
|
||||
(state_user_id, modified, record_date, amount)
|
||||
SELECT user_id as state_user_id, balance_date as modified, balance_date as record_date, amount * 10000 as amount FROM
|
||||
(SELECT user_id as uid, MAX(balance_date) AS date FROM transactions GROUP BY uid) AS t
|
||||
LEFT JOIN transactions ON t.uid = transactions.user_id AND t.date = transactions.balance_date;
|
||||
`)
|
||||
|
||||
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `decay` to `dec_decay`;')
|
||||
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `balance` to `dec_balance`;')
|
||||
await queryFn('ALTER TABLE `transactions` RENAME COLUMN `amount` to `dec_amount`;')
|
||||
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_diff_balance` decimal(40,20) DEFAULT NULL AFTER linked_transaction_id;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_old_balance` decimal(40,20) DEFAULT NULL AFTER linked_transaction_id;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_diff_send_sender_final_balance` decimal(40,20) DEFAULT NULL AFTER linked_transaction_id;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_send_sender_final_balance` decimal(40,20) DEFAULT NULL AFTER linked_transaction_id;',
|
||||
)
|
||||
await queryFn('ALTER TABLE `transactions` ADD COLUMN `balance` bigint(20) DEFAULT 0 AFTER memo;')
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `send_sender_final_balance` bigint(20) DEFAULT NULL AFTER memo;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `transactions` ADD COLUMN `amount` bigint(20) DEFAULT NULL AFTER decay_start;',
|
||||
)
|
||||
|
||||
await queryFn(`
|
||||
UPDATE transactions SET
|
||||
temp_dec_diff_balance = 0,
|
||||
temp_dec_old_balance = dec_balance,
|
||||
temp_dec_diff_send_sender_final_balance = 0,
|
||||
temp_dec_send_sender_final_balance = dec_balance,
|
||||
balance = dec_balance * 10000,
|
||||
send_sender_final_balance = dec_balance * 10000,
|
||||
amount = dec_amount * 10000;
|
||||
`)
|
||||
|
||||
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `amount` bigint(20) NOT NULL;')
|
||||
}
|
||||
@ -10,15 +10,15 @@
|
||||
"scripts": {
|
||||
"build": "mkdir -p build/src/config/ && cp src/config/*.txt build/src/config/ && tsc --build",
|
||||
"clean": "tsc --build --clean",
|
||||
"up": "node build/src/index.js up",
|
||||
"down": "node build/src/index.js down",
|
||||
"reset": "node build/src/index.js reset",
|
||||
"dev_up": "ts-node src/index.ts up",
|
||||
"dev_down": "ts-node src/index.ts down",
|
||||
"dev_reset": "ts-node src/index.ts reset",
|
||||
"up": "TZ=UTC node build/src/index.js up",
|
||||
"down": "TZ=UTC node build/src/index.js down",
|
||||
"reset": "TZ=UTC node build/src/index.js reset",
|
||||
"dev_up": "TZ=UTC ts-node src/index.ts up",
|
||||
"dev_down": "TZ=UTC ts-node src/index.ts down",
|
||||
"dev_reset": "TZ=UTC ts-node src/index.ts reset",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
||||
"seed:config": "ts-node ./node_modules/typeorm-seeding/dist/cli.js config",
|
||||
"seed": "ts-node src/index.ts seed"
|
||||
"seed": "TZ=UTC ts-node src/index.ts seed"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/faker": "^5.5.9",
|
||||
@ -38,6 +38,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"crypto": "^1.0.1",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"faker": "^5.5.3",
|
||||
"mysql2": "^2.3.0",
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import Faker from 'faker'
|
||||
import { define } from 'typeorm-seeding'
|
||||
import { Balance } from '../../entity/Balance'
|
||||
import { BalanceContext } from '../interface/TransactionContext'
|
||||
|
||||
define(Balance, (faker: typeof Faker, context?: BalanceContext) => {
|
||||
if (!context || !context.user) {
|
||||
throw new Error('Balance: No user present!')
|
||||
}
|
||||
|
||||
const balance = new Balance()
|
||||
balance.modified = context.modified ? context.modified : faker.date.recent()
|
||||
balance.recordDate = context.recordDate ? context.recordDate : faker.date.recent()
|
||||
balance.amount = context.amount ? context.amount : 10000000
|
||||
balance.user = context.user
|
||||
|
||||
return balance
|
||||
})
|
||||
@ -2,6 +2,7 @@ import Faker from 'faker'
|
||||
import { define } from 'typeorm-seeding'
|
||||
import { Transaction } from '../../entity/Transaction'
|
||||
import { TransactionContext } from '../interface/TransactionContext'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
define(Transaction, (faker: typeof Faker, context?: TransactionContext) => {
|
||||
if (!context) {
|
||||
@ -12,11 +13,12 @@ define(Transaction, (faker: typeof Faker, context?: TransactionContext) => {
|
||||
transaction.typeId = context.typeId // || 2
|
||||
transaction.userId = context.userId
|
||||
transaction.amount = context.amount
|
||||
transaction.balance = context.balance
|
||||
transaction.decay = new Decimal(0) // context.decay
|
||||
transaction.memo = context.memo
|
||||
transaction.creationDate = context.creationDate || new Date()
|
||||
// transaction.sendReceiverPublicKey = context.sendReceiverPublicKey || null
|
||||
transaction.linkedUserId = context.sendReceiverUserId || null
|
||||
transaction.sendSenderFinalBalance = context.sendSenderFinalBalance || null
|
||||
|
||||
return transaction
|
||||
})
|
||||
|
||||
@ -1,20 +1,12 @@
|
||||
import { User } from '../../entity/User'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export interface TransactionContext {
|
||||
typeId: number
|
||||
userId: number
|
||||
balance: BigInt
|
||||
balance: Decimal
|
||||
balanceDate: Date
|
||||
amount: BigInt
|
||||
amount: Decimal
|
||||
memo: string
|
||||
creationDate?: Date
|
||||
sendReceiverUserId?: number
|
||||
sendSenderFinalBalance?: BigInt
|
||||
}
|
||||
|
||||
export interface BalanceContext {
|
||||
modified?: Date
|
||||
recordDate?: Date
|
||||
amount?: number
|
||||
user?: User
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export interface UserInterface {
|
||||
// from user
|
||||
email?: string
|
||||
@ -24,10 +26,7 @@ export interface UserInterface {
|
||||
// flag for balance (creation of 1000 GDD)
|
||||
addBalance?: boolean
|
||||
// balance
|
||||
balanceModified?: Date
|
||||
recordDate?: Date
|
||||
creationDate?: Date
|
||||
amount?: number
|
||||
creationTxHash?: Buffer
|
||||
signature?: Buffer
|
||||
amount?: Decimal
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { UserContext, ServerUserContext } from '../../interface/UserContext'
|
||||
import { BalanceContext, TransactionContext } from '../../interface/TransactionContext'
|
||||
import { TransactionContext } from '../../interface/TransactionContext'
|
||||
import { UserInterface } from '../../interface/UserInterface'
|
||||
import { User } from '../../../entity/User'
|
||||
import { ServerUser } from '../../../entity/ServerUser'
|
||||
import { Balance } from '../../../entity/Balance'
|
||||
import { Transaction } from '../../../entity/Transaction'
|
||||
import { Factory } from 'typeorm-seeding'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const userSeeder = async (factory: Factory, userData: UserInterface): Promise<void> => {
|
||||
const user = await factory(User)(createUserContext(userData)).create()
|
||||
@ -16,7 +16,6 @@ export const userSeeder = async (factory: Factory, userData: UserInterface): Pro
|
||||
|
||||
if (userData.addBalance) {
|
||||
// create some GDD for the user
|
||||
await factory(Balance)(createBalanceContext(userData, user)).create()
|
||||
await factory(Transaction)(
|
||||
createTransactionContext(userData, user, 1, 'Herzlich Willkommen bei Gradido!'),
|
||||
).create()
|
||||
@ -52,15 +51,6 @@ const createServerUserContext = (context: UserInterface): ServerUserContext => {
|
||||
}
|
||||
}
|
||||
|
||||
const createBalanceContext = (context: UserInterface, user: User): BalanceContext => {
|
||||
return {
|
||||
modified: context.balanceModified,
|
||||
recordDate: context.recordDate,
|
||||
amount: context.amount,
|
||||
user,
|
||||
}
|
||||
}
|
||||
|
||||
const createTransactionContext = (
|
||||
context: UserInterface,
|
||||
user: User,
|
||||
@ -70,8 +60,8 @@ const createTransactionContext = (
|
||||
return {
|
||||
typeId: type,
|
||||
userId: user.id,
|
||||
amount: BigInt(context.amount || 100000),
|
||||
balance: BigInt(context.amount || 100000),
|
||||
amount: context.amount || new Decimal(1000),
|
||||
balance: context.amount || new Decimal(1000),
|
||||
balanceDate: new Date(context.recordDate || Date.now()),
|
||||
memo,
|
||||
creationDate: context.creationDate,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { UserInterface } from '../../interface/UserInterface'
|
||||
|
||||
export const bibiBloxberg: UserInterface = {
|
||||
@ -19,16 +20,7 @@ export const bibiBloxberg: UserInterface = {
|
||||
'knife normal level all hurdle crucial color avoid warrior stadium road bachelor affair topple hawk pottery right afford immune two ceiling budget glance hour ',
|
||||
isAdmin: false,
|
||||
addBalance: true,
|
||||
balanceModified: new Date('2021-11-30T10:37:11'),
|
||||
recordDate: new Date('2021-11-30T10:37:11'),
|
||||
creationDate: new Date('2021-08-01 00:00:00'),
|
||||
amount: 10000000,
|
||||
creationTxHash: Buffer.from(
|
||||
'51103dc0fc2ca5d5d75a9557a1e899304e5406cfdb1328d8df6414d527b0118100000000000000000000000000000000',
|
||||
'hex',
|
||||
),
|
||||
signature: Buffer.from(
|
||||
'2a2c71f3e41adc060bbc3086577e2d57d24eeeb0a7727339c3f85aad813808f601d7e1df56a26e0929d2e67fc054fca429ccfa283ed2782185c7f009fe008f0c',
|
||||
'hex',
|
||||
),
|
||||
amount: new Decimal(1000),
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { UserInterface } from '../../interface/UserInterface'
|
||||
|
||||
export const bobBaumeister: UserInterface = {
|
||||
@ -19,16 +20,7 @@ export const bobBaumeister: UserInterface = {
|
||||
'detail master source effort unable waste tilt flush domain orchard art truck hint barrel response gate impose peanut secret merry three uncle wink resource ',
|
||||
isAdmin: false,
|
||||
addBalance: true,
|
||||
balanceModified: new Date('2021-11-30T10:37:14'),
|
||||
recordDate: new Date('2021-11-30T10:37:14'),
|
||||
creationDate: new Date('2021-08-01 00:00:00'),
|
||||
amount: 10000000,
|
||||
creationTxHash: Buffer.from(
|
||||
'be095dc87acb94987e71168fee8ecbf50ecb43a180b1006e75d573b35725c69c00000000000000000000000000000000',
|
||||
'hex',
|
||||
),
|
||||
signature: Buffer.from(
|
||||
'1fbd6b9a3d359923b2501557f3bc79fa7e428127c8090fb16bc490b4d87870ab142b3817ddd902d22f0b26472a483233784a0e460c0622661752a13978903905',
|
||||
'hex',
|
||||
),
|
||||
amount: new Decimal(1000),
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { UserInterface } from '../../interface/UserInterface'
|
||||
|
||||
export const raeuberHotzenplotz: UserInterface = {
|
||||
@ -19,16 +20,7 @@ export const raeuberHotzenplotz: UserInterface = {
|
||||
'gospel trip tenant mouse spider skill auto curious man video chief response same little over expire drum display fancy clinic keen throw urge basket ',
|
||||
isAdmin: false,
|
||||
addBalance: true,
|
||||
balanceModified: new Date('2021-11-30T10:37:13'),
|
||||
recordDate: new Date('2021-11-30T10:37:13'),
|
||||
creationDate: new Date('2021-08-01 00:00:00'),
|
||||
amount: 10000000,
|
||||
creationTxHash: Buffer.from(
|
||||
'23ba44fd84deb59b9f32969ad0cb18bfa4588be1bdb99c396888506474c16c1900000000000000000000000000000000',
|
||||
'hex',
|
||||
),
|
||||
signature: Buffer.from(
|
||||
'756d3da061687c575d1dbc5073908f646aa5f498b0927b217c83b48af471450e571dfe8421fb8e1f1ebd1104526b7e7c6fa78684e2da59c8f7f5a8dc3d9e5b0b',
|
||||
'hex',
|
||||
),
|
||||
amount: new Decimal(1000),
|
||||
}
|
||||
|
||||
19
database/src/typeorm/DecimalTransformer.ts
Normal file
19
database/src/typeorm/DecimalTransformer.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { ValueTransformer } from 'typeorm'
|
||||
|
||||
Decimal.set({
|
||||
precision: 25,
|
||||
rounding: Decimal.ROUND_HALF_UP,
|
||||
})
|
||||
|
||||
export const DecimalTransformer: ValueTransformer = {
|
||||
/**
|
||||
* Used to marshal Decimal when writing to the database.
|
||||
*/
|
||||
to: (decimal: Decimal | null): string | null => (decimal ? decimal.toString() : null),
|
||||
|
||||
/**
|
||||
* Used to unmarshal Decimal when reading from the database.
|
||||
*/
|
||||
from: (decimal: string | null): Decimal | null => (decimal ? new Decimal(decimal) : null),
|
||||
}
|
||||
@ -560,6 +560,11 @@ decamelize@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
|
||||
|
||||
decimal.js-light@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
|
||||
integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
|
||||
|
||||
deep-is@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||
|
||||
@ -57,7 +57,7 @@ export const createUser = gql`
|
||||
`
|
||||
|
||||
export const sendCoins = gql`
|
||||
mutation($email: String!, $amount: Float!, $memo: String!) {
|
||||
mutation($email: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(email: $email, amount: $amount, memo: $memo)
|
||||
}
|
||||
`
|
||||
|
||||
@ -55,30 +55,26 @@ export const transactionsQuery = gql`
|
||||
order: $order
|
||||
onlyCreations: $onlyCreations
|
||||
) {
|
||||
gdtSum
|
||||
balanceGDT
|
||||
count
|
||||
balance
|
||||
decay
|
||||
decayDate
|
||||
decayStartBlock
|
||||
transactions {
|
||||
type
|
||||
balance
|
||||
decayStart
|
||||
decayEnd
|
||||
decayDuration
|
||||
id
|
||||
typeId
|
||||
amount
|
||||
balanceDate
|
||||
memo
|
||||
transactionId
|
||||
name
|
||||
email
|
||||
date
|
||||
decay {
|
||||
balance
|
||||
decayStart
|
||||
decayEnd
|
||||
decayDuration
|
||||
decayStartBlock
|
||||
linkedUser {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
decay {
|
||||
decay
|
||||
start
|
||||
end
|
||||
duration
|
||||
}
|
||||
firstTransaction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user