From 03b63f8c455073d9ce450d052eee10988b0f9479 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Mon, 14 Aug 2023 22:45:17 +0200 Subject: [PATCH 01/36] Fix Vue error in groupProposalSettings --- frontend/model/contracts/group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 2d4225a49..58607a8e1 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -415,7 +415,7 @@ sbp('chelonia/defineContract', { }, groupProposalSettings (state, getters) { return (proposalType = PROPOSAL_GENERIC) => { - return getters.groupSettings.proposals[proposalType] + return getters.groupSettings.proposals?.[proposalType] } }, groupCurrency (state, getters) { From 3b9e24633afbc1523815ac2350647e1e332d8eea Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Mon, 14 Aug 2023 22:46:37 +0200 Subject: [PATCH 02/36] Remove unused paymentTotalFromUserToUser getter --- frontend/model/contracts/group.js | 33 +------------------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 58607a8e1..321d1f7b1 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -16,7 +16,7 @@ import { createPaymentInfo, paymentHashesFromPaymentPeriod } from './shared/func import { merge, deepEqualJSONType, omit, cloneDeep } from './shared/giLodash.js' import { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, periodStampGivenDate, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js' import { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js' -import currencies, { saferFloat } from './shared/currencies.js' +import currencies from './shared/currencies.js' import { inviteType, chatRoomAttributesType } from './shared/types.js' import { arrayOf, mapOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js' @@ -348,37 +348,6 @@ sbp('chelonia/defineContract', { return getters.periodAfterPeriod(periodStamp) } }, - paymentTotalFromUserToUser (state, getters) { - return (fromUser, toUser, periodStamp) => { - const payments = getters.currentGroupState.payments - const periodPayments = getters.groupPeriodPayments - const { paymentsFrom, mincomeExchangeRate } = periodPayments[periodStamp] || {} - // NOTE: @babel/plugin-proposal-optional-chaining would come in super-handy - // here, but I couldn't get it to work with our linter. :( - // https://github.com/babel/babel-eslint/issues/511 - const total = (((paymentsFrom || {})[fromUser] || {})[toUser] || []).reduce((a, hash) => { - const payment = payments[hash] - let { amount, exchangeRate, status } = payment.data - if (status !== PAYMENT_COMPLETED) { - return a - } - const paymentCreatedPeriodStamp = getters.periodStampGivenDate(payment.meta.createdDate) - // if this payment is from a previous period, then make sure to take into account - // any proposals that passed in between the payment creation and the payment - // completion that modified the group currency by multiplying both period's - // exchange rates - if (periodStamp !== paymentCreatedPeriodStamp) { - if (paymentCreatedPeriodStamp !== getters.periodBeforePeriod(periodStamp)) { - console.warn(`paymentTotalFromUserToUser: super old payment shouldn't exist, ignoring! (curPeriod=${periodStamp})`, JSON.stringify(payment)) - return a - } - exchangeRate *= periodPayments[paymentCreatedPeriodStamp].mincomeExchangeRate - } - return a + (amount * exchangeRate * mincomeExchangeRate) - }, 0) - return saferFloat(total) - } - }, paymentHashesForPeriod (state, getters) { return (periodStamp) => { const periodPayments = getters.groupPeriodPayments[periodStamp] From b6c71c10e05583562b77403c14c6c399c6403e95 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Wed, 16 Aug 2023 23:30:08 +0200 Subject: [PATCH 03/36] Add groupSortedPeriodKeys getter in group.js --- frontend/model/contracts/group.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 321d1f7b1..22dd8dedb 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -312,6 +312,12 @@ sbp('chelonia/defineContract', { groupMincomeCurrency (state, getters) { return getters.groupSettings.mincomeCurrency }, + // Oldest period key first. + groupSortedPeriodKeys (state, getters) { + // The .sort() call might be only necessary in older browser which don't maintain object key ordering. + // A comparator function isn't required for now since our keys are ISO strings. + return Object.keys(getters.currentGroupState.paymentsByPeriod ?? {}).sort() + }, periodStampGivenDate (state, getters) { return (recentDate: string | Date) => { if (typeof recentDate !== 'string') { From a922cb47067e1cedb2c9ad42b35002d6b38a65d4 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Wed, 16 Aug 2023 23:34:13 +0200 Subject: [PATCH 04/36] Update getters to support variable period length --- frontend/model/contracts/group.js | 139 +++++++++++++++--- frontend/model/contracts/manifests.json | 2 +- .../contributions/SupportHistory.vue | 24 +-- .../containers/contributions/TodoHistory.vue | 17 +-- .../containers/payments/PaymentDetail.vue | 9 +- .../payments/PaymentRowReceived.vue | 13 +- .../containers/payments/PaymentRowSent.vue | 13 +- .../containers/payments/PaymentsMixin.js | 67 ++++++++- frontend/views/pages/Payments.vue | 3 +- test/cypress/integration/group-paying.spec.js | 68 ++++++++- 10 files changed, 288 insertions(+), 67 deletions(-) diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 22dd8dedb..1d3ed9718 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -14,7 +14,7 @@ import { import { paymentStatusType, paymentType, PAYMENT_COMPLETED } from './shared/payments/index.js' import { createPaymentInfo, paymentHashesFromPaymentPeriod } from './shared/functions.js' import { merge, deepEqualJSONType, omit, cloneDeep } from './shared/giLodash.js' -import { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, periodStampGivenDate, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js' +import { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, dateIsWithinPeriod, DAYS_MILLIS } from './shared/time.js' import { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js' import currencies from './shared/currencies.js' import { inviteType, chatRoomAttributesType } from './shared/types.js' @@ -318,32 +318,132 @@ sbp('chelonia/defineContract', { // A comparator function isn't required for now since our keys are ISO strings. return Object.keys(getters.currentGroupState.paymentsByPeriod ?? {}).sort() }, + // Returns either the known period stamp for the given date, + // or the predicted one according to the period length. + // May return 'undefined', in which case the caller should check archived data. periodStampGivenDate (state, getters) { return (recentDate: string | Date) => { - if (typeof recentDate !== 'string') { - recentDate = recentDate.toISOString() - } + if (typeof recentDate !== 'string') recentDate = recentDate.toISOString() + if (!isPeriodStamp(recentDate)) throw new TypeError('must be date or isostring') const { distributionDate, distributionPeriodLength } = getters.groupSettings - - if (!distributionDate) return null - - return periodStampGivenDate({ - recentDate, - periodStart: distributionDate, - periodLength: distributionPeriodLength - }) + if (!distributionDate) return + const sortedPeriodKeys = getters.groupSortedPeriodKeys + // Maybe the distribution date has just been updated, + // but the corresponding period has not started or is not stored yet. + // So we try to use the distribution date first. + if (recentDate >= distributionDate) { + // Only extrapolate one period length in the future. + const extrapolatedDistributionDate = addTimeToDate( + distributionDate, distributionPeriodLength + ).toISOString() + if (recentDate >= extrapolatedDistributionDate) { + return dateToPeriodStamp(extrapolatedDistributionDate) + } + return dateToPeriodStamp(distributionDate) + } + // For dates before the distribution date, we check the stored data first, + // as simply substracting the default period length won't give the previous period stamp + // if the distribution date was updated during that period. + if (!sortedPeriodKeys.length) { + // Looks like we're in the waiting period but haven't stored it yet. + const waitingPeriodStamp = dateToPeriodStamp( + addTimeToDate(distributionDate, -distributionPeriodLength) + ) + return recentDate >= waitingPeriodStamp ? waitingPeriodStamp : undefined + } + const oldestKnownStamp = sortedPeriodKeys[0] + const latestKnownStamp = sortedPeriodKeys[sortedPeriodKeys.length - 1] + if (recentDate < oldestKnownStamp) return + if (recentDate >= latestKnownStamp) { + const extrapolatedPeriodStamp = dateToPeriodStamp( + addTimeToDate(dateFromPeriodStamp(latestKnownStamp), distributionPeriodLength) + ) + if (recentDate >= extrapolatedPeriodStamp) return extrapolatedPeriodStamp + return latestKnownStamp + } + for (let i = 1; i < sortedPeriodKeys.length; i++) { + if (recentDate < sortedPeriodKeys) return sortedPeriodKeys[i - 1] + } + // This should not happen } }, + // May return 'undefined', in which case the caller should check archived data. periodBeforePeriod (state, getters) { return (periodStamp: string) => { - const len = getters.groupSettings.distributionPeriodLength - return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -len)) + if (!isPeriodStamp(periodStamp)) throw new TypeError('must be periodStamp') + const { distributionDate, distributionPeriodLength } = getters.groupSettings + if (!distributionDate) return + // This is not always the current period stamp. + const distributionDateStamp = dateToPeriodStamp(distributionDate) + const sortedPeriodKeys = getters.groupSortedPeriodKeys + const latestKnownStamp = sortedPeriodKeys[sortedPeriodKeys.length - 1] + // Maybe the distribution date has just been updated, + // but the corresponding period has not started or is not stored yet. + // So we try to use the distribution date first. + if (periodStamp > distributionDateStamp) { + // Only extrapolate one period length in the future. + const onePeriodLenghtAhead = addTimeToDate(distributionDate, distributionPeriodLength) + if (periodStamp === dateToPeriodStamp(onePeriodLenghtAhead)) return distributionDate + else return + } + if (periodStamp === distributionDateStamp) { + if (sortedPeriodKeys.length) { + // If the distribution date doesn't match the latest known period stamp, + // then either that stamp is for the waiting period, + // or the distribution date has just been updated. + // In both cases we can return it. + if (latestKnownStamp !== distributionDateStamp) return latestKnownStamp + // Otherwise it's a normal period, therefore substracting the period length would not be reliable. + else return sortedPeriodKeys[sortedPeriodKeys.length - 2] ?? undefined + } else { + // If no period has been stored yet, then we're in the waiting period and can do arithmetic. + return dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength)) + } + } + const index = sortedPeriodKeys.indexOf(periodStamp) + if (index === -1) { + // Maybe the given stamp is wrong and has no associated period, + // but is one period length ahead of a known one. + // TODO: just return 'undefined' when sure it's always safe. + const maybePreviousStamp = dateToPeriodStamp( + addTimeToDate(dateFromPeriodStamp(periodStamp), -distributionPeriodLength) + ) + return sortedPeriodKeys.includes(maybePreviousStamp) ? maybePreviousStamp : undefined + } + // If index is 0 then the caller will have to check the archive. + if (index === 0) return + return sortedPeriodKeys[index - 1] } }, + // May return 'undefined', in which case the caller should check archived data. periodAfterPeriod (state, getters) { return (periodStamp: string) => { - const len = getters.groupSettings.distributionPeriodLength - return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), len)) + if (!isPeriodStamp(periodStamp)) throw new TypeError('must be periodStamp') + const { distributionDate, distributionPeriodLength } = getters.groupSettings + if (!distributionDate) return + // This is not always the current period stamp. + const distributionDateStamp = dateToPeriodStamp(distributionDate) + const sortedPeriodKeys = getters.groupSortedPeriodKeys + + // Maybe the distribution date has just been updated, + // and the corresponding period has not started or is not stored yet. + if (periodStamp > distributionDateStamp) return + if (periodStamp === distributionDateStamp) { + return dateToPeriodStamp(addTimeToDate(distributionDate, distributionPeriodLength)) + } + // Maybe we're in the waiting period but haven't stored it yet. + if (!sortedPeriodKeys.length) { + const waitingPeriodStamp = dateToPeriodStamp( + addTimeToDate(distributionDate, -distributionPeriodLength) + ) + return periodStamp === waitingPeriodStamp ? distributionDate : undefined + } + const index = sortedPeriodKeys.indexOf(periodStamp) + if (index === -1) return + // Maybe the given stamp is the last stored one but doesn't match the distribution date. + if (index === sortedPeriodKeys.length - 1) return dateToPeriodStamp(distributionDate) + // Now 'index + 1' is always a valid index. + return sortedPeriodKeys[index + 1] } }, dueDateForPeriod (state, getters) { @@ -1237,8 +1337,11 @@ sbp('chelonia/defineContract', { process ({ meta }, { state, getters }) { const period = getters.periodStampGivenDate(meta.createdDate) const current = getters.groupSettings?.distributionDate - - if (current !== period) { + const inWaitingPeriod = !current || new Date().toISOString() < current + // Maybe we're updating the distribution date while in the waiting period. + if (inWaitingPeriod && meta.createdDate !== current) { + getters.groupSettings.distributionDate = meta.createdDate + } else if (current !== period) { // right before updating to the new distribution period, make sure to update various payment-related group streaks. updateGroupStreaks({ state, getters }) getters.groupSettings.distributionDate = period diff --git a/frontend/model/contracts/manifests.json b/frontend/model/contracts/manifests.json index 152dee7c0..f83385210 100644 --- a/frontend/model/contracts/manifests.json +++ b/frontend/model/contracts/manifests.json @@ -1,7 +1,7 @@ { "manifests": { "gi.contracts/chatroom": "21XWnNS9zeT2tws6KkJqkibLGfTivPdiGYbSjjqFEkDjL5Q775", - "gi.contracts/group": "21XWnNGw4mX4hBGLwq5TXPY1JWcBzEWXFqRhkWWdeBGvBCVhtj", + "gi.contracts/group": "21XWnNK3pmNAdgXLe7rkjVjEQ1z3Lccp3o7YpZ64bAr18VNPAW", "gi.contracts/identity": "21XWnNN7wGNxzFZmiS4ft2TumavYYAgemSuFrGavTVJpxqtnyg", "gi.contracts/mailbox": "21XWnNHJ7MALCinR7vnu4GM82nq5DLhe6nhEDzruTuTw9CTnXP" } diff --git a/frontend/views/containers/contributions/SupportHistory.vue b/frontend/views/containers/contributions/SupportHistory.vue index dc1ed3aa3..25ae1ac4d 100644 --- a/frontend/views/containers/contributions/SupportHistory.vue +++ b/frontend/views/containers/contributions/SupportHistory.vue @@ -20,8 +20,6 @@ div(:class='isReady ? "" : "c-ready"') diff --git a/frontend/views/containers/payments/PaymentRowSent.vue b/frontend/views/containers/payments/PaymentRowSent.vue index 9f2fed185..33b443e33 100644 --- a/frontend/views/containers/payments/PaymentRowSent.vue +++ b/frontend/views/containers/payments/PaymentRowSent.vue @@ -90,9 +90,6 @@ export default ({ return comparePeriodStamps(this.payment.period, this.currentPaymentPeriod) < 0 } }, - created () { - this.updatePayment() - }, methods: { humanDate, openModal (name, props) { @@ -114,15 +111,10 @@ export default ({ console.error(e) alert(e.message) } - }, - async updatePaymentRelativeTo (paymentDate) { - this.relativeTo = await this.historicalPeriodStampGivenDate(paymentDate) } }, - watch: { - payment (to, from) { - this.updatePaymentRelativeTo(to.date) - } + async mounted () { + this.relativeTo = await this.historicalPeriodStampGivenDate(this.payment.date) } }: Object) From 2dd21025a9711e7db69df74606df46e8585d9fd7 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sat, 23 Sep 2023 16:02:47 +0200 Subject: [PATCH 26/36] Fix pedantic Flow error --- frontend/model/contracts/shared/time.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/model/contracts/shared/time.js b/frontend/model/contracts/shared/time.js index 700592497..d8674ff9b 100644 --- a/frontend/model/contracts/shared/time.js +++ b/frontend/model/contracts/shared/time.js @@ -22,6 +22,7 @@ export function periodStampsForDate ( date: Date | string, { knownSortedStamps, periodLength }: { knownSortedStamps: string[], periodLength: number } ): Object { + // $FlowFixMe - Pedantic '[method-unbinding]' error if (!(isIsoString(date) || Object.prototype.toString.call(date) === '[object Date]')) { throw new TypeError('must be ISO string or Date object') } From cd18f6231aab3b8bfd17beb8ce2492c3eb70c71a Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sat, 23 Sep 2023 16:37:20 +0200 Subject: [PATCH 27/36] Pin contracts to 0.1.8 --- contracts/0.1.8/group-slim.js | 168 ++++++++++------------ contracts/0.1.8/group.0.1.8.manifest.json | 2 +- contracts/0.1.8/group.js | 168 ++++++++++------------ 3 files changed, 149 insertions(+), 189 deletions(-) diff --git a/contracts/0.1.8/group-slim.js b/contracts/0.1.8/group-slim.js index 4feb27616..a28e85b4e 100644 --- a/contracts/0.1.8/group-slim.js +++ b/contracts/0.1.8/group-slim.js @@ -382,12 +382,61 @@ ${this.getErrorInfo()}`; var HOURS_MILLIS = 60 * MINS_MILLIS; var DAYS_MILLIS = 24 * HOURS_MILLIS; var MONTHS_MILLIS = 30 * DAYS_MILLIS; + var plusOnePeriodLength = (timestamp, periodLength) => dateToPeriodStamp(addTimeToDate(timestamp, periodLength)); + var minusOnePeriodLength = (timestamp, periodLength) => dateToPeriodStamp(addTimeToDate(timestamp, -periodLength)); + function periodStampsForDate(date, { knownSortedStamps, periodLength }) { + if (!(isIsoString(date) || Object.prototype.toString.call(date) === "[object Date]")) { + throw new TypeError("must be ISO string or Date object"); + } + const timestamp = typeof date === "string" ? date : date.toISOString(); + let previous, current, next; + if (knownSortedStamps.length) { + const latest = knownSortedStamps[knownSortedStamps.length - 1]; + if (timestamp >= latest) { + current = periodStampGivenDate({ recentDate: timestamp, periodStart: latest, periodLength }); + next = plusOnePeriodLength(current, periodLength); + previous = current > latest ? minusOnePeriodLength(current, periodLength) : knownSortedStamps[knownSortedStamps.length - 2]; + } else { + for (let i = knownSortedStamps.length - 2; i >= 0; i--) { + if (timestamp >= knownSortedStamps[i]) { + current = knownSortedStamps[i]; + next = knownSortedStamps[i + 1]; + previous = knownSortedStamps[i - 1]; + break; + } + } + } + } + return { previous, current, next }; + } function dateToPeriodStamp(date) { return new Date(date).toISOString(); } function dateFromPeriodStamp(daystamp) { return new Date(daystamp); } + function periodStampGivenDate({ recentDate, periodStart, periodLength }) { + const periodStartDate = dateFromPeriodStamp(periodStart); + let nextPeriod = addTimeToDate(periodStartDate, periodLength); + const curDate = new Date(recentDate); + let curPeriod; + if (curDate < nextPeriod) { + if (curDate >= periodStartDate) { + return periodStart; + } else { + curPeriod = periodStartDate; + do { + curPeriod = addTimeToDate(curPeriod, -periodLength); + } while (curDate < curPeriod); + } + } else { + do { + curPeriod = nextPeriod; + nextPeriod = addTimeToDate(nextPeriod, periodLength); + } while (curDate >= nextPeriod); + } + return dateToPeriodStamp(curPeriod); + } function dateIsWithinPeriod({ date, periodStart, periodLength }) { const dateObj = new Date(date); const start = dateFromPeriodStamp(periodStart); @@ -1008,106 +1057,40 @@ ${this.getErrorInfo()}`; return getters.groupSettings.mincomeCurrency; }, groupSortedPeriodKeys(state, getters) { - return Object.keys(getters.currentGroupState.paymentsByPeriod ?? {}).sort(); + const { distributionDate, distributionPeriodLength } = getters.groupSettings; + if (!distributionDate) + return []; + const keys = Object.keys(getters.groupPeriodPayments).sort(); + if (!keys.length && MAX_SAVED_PERIODS > 0) { + keys.push(dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength))); + } + if (keys[keys.length - 1] !== distributionDate) { + keys.push(distributionDate); + } + return keys; }, periodStampGivenDate(state, getters) { - return (recentDate) => { - if (typeof recentDate !== "string") - recentDate = recentDate.toISOString(); - if (!isIsoString(recentDate)) - throw new TypeError("must be date or isostring"); - const { distributionDate, distributionPeriodLength } = getters.groupSettings; - if (!distributionDate) - return; - const sortedPeriodKeys = getters.groupSortedPeriodKeys; - if (recentDate >= distributionDate) { - const extrapolatedDistributionDate = addTimeToDate(distributionDate, distributionPeriodLength).toISOString(); - if (recentDate >= extrapolatedDistributionDate) { - return dateToPeriodStamp(extrapolatedDistributionDate); - } - return dateToPeriodStamp(distributionDate); - } - if (!sortedPeriodKeys.length) { - const waitingPeriodStamp = dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength)); - return recentDate >= dateFromPeriodStamp(waitingPeriodStamp).toISOString() ? waitingPeriodStamp : void 0; - } - const oldestKnownStamp = sortedPeriodKeys[0]; - const latestKnownStamp = sortedPeriodKeys[sortedPeriodKeys.length - 1]; - if (recentDate < oldestKnownStamp) - return; - if (recentDate >= latestKnownStamp) { - const extrapolatedPeriodStamp = dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(latestKnownStamp), distributionPeriodLength)); - if (recentDate >= extrapolatedPeriodStamp) - return extrapolatedPeriodStamp; - return latestKnownStamp; - } - for (let i = 1; i < sortedPeriodKeys.length; i++) { - if (recentDate < sortedPeriodKeys) - return sortedPeriodKeys[i - 1]; - } + return (date) => { + return periodStampsForDate(date, { + knownSortedStamps: getters.groupSortedPeriodKeys, + periodLength: getters.groupSettings.distributionPeriodLength + }).current; }; }, periodBeforePeriod(state, getters) { return (periodStamp) => { - if (!isPeriodStamp(periodStamp)) - throw new TypeError("must be periodStamp"); - const { distributionDate, distributionPeriodLength } = getters.groupSettings; - if (!distributionDate) - return; - const distributionDateStamp = dateToPeriodStamp(distributionDate); - const sortedPeriodKeys = getters.groupSortedPeriodKeys; - const latestKnownStamp = sortedPeriodKeys[sortedPeriodKeys.length - 1]; - if (periodStamp > distributionDateStamp) { - const onePeriodLenghtAhead = addTimeToDate(distributionDate, distributionPeriodLength); - if (periodStamp === dateToPeriodStamp(onePeriodLenghtAhead)) - return distributionDate; - else - return; - } - if (periodStamp === distributionDateStamp) { - if (sortedPeriodKeys.length) { - if (latestKnownStamp !== distributionDateStamp) - return latestKnownStamp; - else - return sortedPeriodKeys[sortedPeriodKeys.length - 2] ?? void 0; - } else { - return dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength)); - } - } - const index = sortedPeriodKeys.indexOf(periodStamp); - if (index === -1) { - const maybePreviousStamp = dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -distributionPeriodLength)); - return sortedPeriodKeys.includes(maybePreviousStamp) ? maybePreviousStamp : void 0; - } - if (index === 0) - return; - return sortedPeriodKeys[index - 1]; + return periodStampsForDate(periodStamp, { + knownSortedStamps: getters.groupSortedPeriodKeys, + periodLength: getters.groupSettings.distributionPeriodLength + }).previous; }; }, periodAfterPeriod(state, getters) { return (periodStamp) => { - if (!isPeriodStamp(periodStamp)) - throw new TypeError("must be periodStamp"); - const { distributionDate, distributionPeriodLength } = getters.groupSettings; - if (!distributionDate) - return; - const distributionDateStamp = dateToPeriodStamp(distributionDate); - const sortedPeriodKeys = getters.groupSortedPeriodKeys; - if (periodStamp > distributionDateStamp) - return; - if (periodStamp === distributionDateStamp) { - return dateToPeriodStamp(addTimeToDate(distributionDate, distributionPeriodLength)); - } - if (!sortedPeriodKeys.length) { - const waitingPeriodStamp = dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength)); - return periodStamp === waitingPeriodStamp ? distributionDate : void 0; - } - const index = sortedPeriodKeys.indexOf(periodStamp); - if (index === -1) - return; - if (index === sortedPeriodKeys.length - 1) - return dateToPeriodStamp(distributionDate); - return sortedPeriodKeys[index + 1]; + return periodStampsForDate(periodStamp, { + knownSortedStamps: getters.groupSortedPeriodKeys, + periodLength: getters.groupSettings.distributionPeriodLength + }).next; }; }, dueDateForPeriod(state, getters) { @@ -1860,10 +1843,7 @@ ${this.getErrorInfo()}`; process({ meta }, { state, getters }) { const period = getters.periodStampGivenDate(meta.createdDate); const current = getters.groupSettings?.distributionDate; - const inWaitingPeriod = !current || new Date().toISOString() < current; - if (inWaitingPeriod && meta.createdDate !== current) { - getters.groupSettings.distributionDate = meta.createdDate; - } else if (current !== period) { + if (current !== period) { updateGroupStreaks({ state, getters }); getters.groupSettings.distributionDate = period; } diff --git a/contracts/0.1.8/group.0.1.8.manifest.json b/contracts/0.1.8/group.0.1.8.manifest.json index f833ebb5f..8599321cc 100644 --- a/contracts/0.1.8/group.0.1.8.manifest.json +++ b/contracts/0.1.8/group.0.1.8.manifest.json @@ -1 +1 @@ -{"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.1.8\",\"contract\":{\"hash\":\"21XWnNNihkr5M81xVi7xtx2LnpyV15bDLgDwUsBUoJQZuzj5Xg\",\"file\":\"group.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"group-slim.js\",\"hash\":\"21XWnNRcbLWZRAyFNGGsk1u6a12YVNB7w4VzaBfRTiHN5UCPBj\"}}","signature":{"key":"","signature":""}} \ No newline at end of file +{"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.1.8\",\"contract\":{\"hash\":\"21XWnNJcM1WfRQmw3kXXzWVNCYSCT9WWmZPvQR299XZnoZYn55\",\"file\":\"group.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"group-slim.js\",\"hash\":\"21XWnNFAPqkukffCayHhF27Z75EaWDz3vMVmtNRVNa5kNq6zQY\"}}","signature":{"key":"","signature":""}} \ No newline at end of file diff --git a/contracts/0.1.8/group.js b/contracts/0.1.8/group.js index e36aa6be4..1abc14aaf 100644 --- a/contracts/0.1.8/group.js +++ b/contracts/0.1.8/group.js @@ -9554,12 +9554,61 @@ ${this.getErrorInfo()}`; var HOURS_MILLIS = 60 * MINS_MILLIS; var DAYS_MILLIS = 24 * HOURS_MILLIS; var MONTHS_MILLIS = 30 * DAYS_MILLIS; + var plusOnePeriodLength = (timestamp, periodLength) => dateToPeriodStamp(addTimeToDate(timestamp, periodLength)); + var minusOnePeriodLength = (timestamp, periodLength) => dateToPeriodStamp(addTimeToDate(timestamp, -periodLength)); + function periodStampsForDate(date, { knownSortedStamps, periodLength }) { + if (!(isIsoString(date) || Object.prototype.toString.call(date) === "[object Date]")) { + throw new TypeError("must be ISO string or Date object"); + } + const timestamp = typeof date === "string" ? date : date.toISOString(); + let previous, current, next2; + if (knownSortedStamps.length) { + const latest = knownSortedStamps[knownSortedStamps.length - 1]; + if (timestamp >= latest) { + current = periodStampGivenDate({ recentDate: timestamp, periodStart: latest, periodLength }); + next2 = plusOnePeriodLength(current, periodLength); + previous = current > latest ? minusOnePeriodLength(current, periodLength) : knownSortedStamps[knownSortedStamps.length - 2]; + } else { + for (let i = knownSortedStamps.length - 2; i >= 0; i--) { + if (timestamp >= knownSortedStamps[i]) { + current = knownSortedStamps[i]; + next2 = knownSortedStamps[i + 1]; + previous = knownSortedStamps[i - 1]; + break; + } + } + } + } + return { previous, current, next: next2 }; + } function dateToPeriodStamp(date) { return new Date(date).toISOString(); } function dateFromPeriodStamp(daystamp) { return new Date(daystamp); } + function periodStampGivenDate({ recentDate, periodStart, periodLength }) { + const periodStartDate = dateFromPeriodStamp(periodStart); + let nextPeriod = addTimeToDate(periodStartDate, periodLength); + const curDate = new Date(recentDate); + let curPeriod; + if (curDate < nextPeriod) { + if (curDate >= periodStartDate) { + return periodStart; + } else { + curPeriod = periodStartDate; + do { + curPeriod = addTimeToDate(curPeriod, -periodLength); + } while (curDate < curPeriod); + } + } else { + do { + curPeriod = nextPeriod; + nextPeriod = addTimeToDate(nextPeriod, periodLength); + } while (curDate >= nextPeriod); + } + return dateToPeriodStamp(curPeriod); + } function dateIsWithinPeriod({ date, periodStart, periodLength }) { const dateObj = new Date(date); const start = dateFromPeriodStamp(periodStart); @@ -10132,106 +10181,40 @@ ${this.getErrorInfo()}`; return getters.groupSettings.mincomeCurrency; }, groupSortedPeriodKeys(state, getters) { - return Object.keys(getters.currentGroupState.paymentsByPeriod ?? {}).sort(); + const { distributionDate, distributionPeriodLength } = getters.groupSettings; + if (!distributionDate) + return []; + const keys = Object.keys(getters.groupPeriodPayments).sort(); + if (!keys.length && MAX_SAVED_PERIODS > 0) { + keys.push(dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength))); + } + if (keys[keys.length - 1] !== distributionDate) { + keys.push(distributionDate); + } + return keys; }, periodStampGivenDate(state, getters) { - return (recentDate) => { - if (typeof recentDate !== "string") - recentDate = recentDate.toISOString(); - if (!isIsoString(recentDate)) - throw new TypeError("must be date or isostring"); - const { distributionDate, distributionPeriodLength } = getters.groupSettings; - if (!distributionDate) - return; - const sortedPeriodKeys = getters.groupSortedPeriodKeys; - if (recentDate >= distributionDate) { - const extrapolatedDistributionDate = addTimeToDate(distributionDate, distributionPeriodLength).toISOString(); - if (recentDate >= extrapolatedDistributionDate) { - return dateToPeriodStamp(extrapolatedDistributionDate); - } - return dateToPeriodStamp(distributionDate); - } - if (!sortedPeriodKeys.length) { - const waitingPeriodStamp = dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength)); - return recentDate >= dateFromPeriodStamp(waitingPeriodStamp).toISOString() ? waitingPeriodStamp : void 0; - } - const oldestKnownStamp = sortedPeriodKeys[0]; - const latestKnownStamp = sortedPeriodKeys[sortedPeriodKeys.length - 1]; - if (recentDate < oldestKnownStamp) - return; - if (recentDate >= latestKnownStamp) { - const extrapolatedPeriodStamp = dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(latestKnownStamp), distributionPeriodLength)); - if (recentDate >= extrapolatedPeriodStamp) - return extrapolatedPeriodStamp; - return latestKnownStamp; - } - for (let i = 1; i < sortedPeriodKeys.length; i++) { - if (recentDate < sortedPeriodKeys) - return sortedPeriodKeys[i - 1]; - } + return (date) => { + return periodStampsForDate(date, { + knownSortedStamps: getters.groupSortedPeriodKeys, + periodLength: getters.groupSettings.distributionPeriodLength + }).current; }; }, periodBeforePeriod(state, getters) { return (periodStamp) => { - if (!isPeriodStamp(periodStamp)) - throw new TypeError("must be periodStamp"); - const { distributionDate, distributionPeriodLength } = getters.groupSettings; - if (!distributionDate) - return; - const distributionDateStamp = dateToPeriodStamp(distributionDate); - const sortedPeriodKeys = getters.groupSortedPeriodKeys; - const latestKnownStamp = sortedPeriodKeys[sortedPeriodKeys.length - 1]; - if (periodStamp > distributionDateStamp) { - const onePeriodLenghtAhead = addTimeToDate(distributionDate, distributionPeriodLength); - if (periodStamp === dateToPeriodStamp(onePeriodLenghtAhead)) - return distributionDate; - else - return; - } - if (periodStamp === distributionDateStamp) { - if (sortedPeriodKeys.length) { - if (latestKnownStamp !== distributionDateStamp) - return latestKnownStamp; - else - return sortedPeriodKeys[sortedPeriodKeys.length - 2] ?? void 0; - } else { - return dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength)); - } - } - const index2 = sortedPeriodKeys.indexOf(periodStamp); - if (index2 === -1) { - const maybePreviousStamp = dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -distributionPeriodLength)); - return sortedPeriodKeys.includes(maybePreviousStamp) ? maybePreviousStamp : void 0; - } - if (index2 === 0) - return; - return sortedPeriodKeys[index2 - 1]; + return periodStampsForDate(periodStamp, { + knownSortedStamps: getters.groupSortedPeriodKeys, + periodLength: getters.groupSettings.distributionPeriodLength + }).previous; }; }, periodAfterPeriod(state, getters) { return (periodStamp) => { - if (!isPeriodStamp(periodStamp)) - throw new TypeError("must be periodStamp"); - const { distributionDate, distributionPeriodLength } = getters.groupSettings; - if (!distributionDate) - return; - const distributionDateStamp = dateToPeriodStamp(distributionDate); - const sortedPeriodKeys = getters.groupSortedPeriodKeys; - if (periodStamp > distributionDateStamp) - return; - if (periodStamp === distributionDateStamp) { - return dateToPeriodStamp(addTimeToDate(distributionDate, distributionPeriodLength)); - } - if (!sortedPeriodKeys.length) { - const waitingPeriodStamp = dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength)); - return periodStamp === waitingPeriodStamp ? distributionDate : void 0; - } - const index2 = sortedPeriodKeys.indexOf(periodStamp); - if (index2 === -1) - return; - if (index2 === sortedPeriodKeys.length - 1) - return dateToPeriodStamp(distributionDate); - return sortedPeriodKeys[index2 + 1]; + return periodStampsForDate(periodStamp, { + knownSortedStamps: getters.groupSortedPeriodKeys, + periodLength: getters.groupSettings.distributionPeriodLength + }).next; }; }, dueDateForPeriod(state, getters) { @@ -10984,10 +10967,7 @@ ${this.getErrorInfo()}`; process({ meta }, { state, getters }) { const period = getters.periodStampGivenDate(meta.createdDate); const current = getters.groupSettings?.distributionDate; - const inWaitingPeriod = !current || new Date().toISOString() < current; - if (inWaitingPeriod && meta.createdDate !== current) { - getters.groupSettings.distributionDate = meta.createdDate; - } else if (current !== period) { + if (current !== period) { updateGroupStreaks({ state, getters }); getters.groupSettings.distributionDate = period; } From 44c5d3b22397411e058e81fcdca789b6b52c6b23 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sat, 23 Sep 2023 16:30:06 +0200 Subject: [PATCH 28/36] Move mounted() near the top --- frontend/views/containers/payments/PaymentRowReceived.vue | 6 +++--- frontend/views/containers/payments/PaymentRowSent.vue | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/views/containers/payments/PaymentRowReceived.vue b/frontend/views/containers/payments/PaymentRowReceived.vue index ee7118b87..51c67c3bb 100644 --- a/frontend/views/containers/payments/PaymentRowReceived.vue +++ b/frontend/views/containers/payments/PaymentRowReceived.vue @@ -84,6 +84,9 @@ export default ({ required: true } }, + async mounted () { + this.relativeTo = await this.historicalPeriodStampGivenDate(this.payment.date) + }, computed: { ...mapGetters([ 'withGroupCurrency' @@ -118,9 +121,6 @@ export default ({ alert(e.message) } } - }, - async mounted () { - this.relativeTo = await this.historicalPeriodStampGivenDate(this.payment.date) } }: Object) diff --git a/frontend/views/containers/payments/PaymentRowSent.vue b/frontend/views/containers/payments/PaymentRowSent.vue index 33b443e33..7c0723468 100644 --- a/frontend/views/containers/payments/PaymentRowSent.vue +++ b/frontend/views/containers/payments/PaymentRowSent.vue @@ -77,6 +77,9 @@ export default ({ required: true } }, + async mounted () { + this.relativeTo = await this.historicalPeriodStampGivenDate(this.payment.date) + }, computed: { ...mapGetters([ 'ourGroupProfile', @@ -112,9 +115,6 @@ export default ({ alert(e.message) } } - }, - async mounted () { - this.relativeTo = await this.historicalPeriodStampGivenDate(this.payment.date) } }: Object) From 7023b85116a310d2720d4e5feb6247937fd6c03f Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sat, 23 Sep 2023 16:32:53 +0200 Subject: [PATCH 29/36] Revert humanDate to plain import in MonthOverview.vue --- frontend/views/containers/payments/MonthOverview.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/views/containers/payments/MonthOverview.vue b/frontend/views/containers/payments/MonthOverview.vue index 762878331..5f59c7e3e 100644 --- a/frontend/views/containers/payments/MonthOverview.vue +++ b/frontend/views/containers/payments/MonthOverview.vue @@ -44,7 +44,6 @@ export default ({ ProgressBar }, methods: { - humanDate, statusIsSent (user) { return ['completed', 'pending'].includes(user.status) }, @@ -66,10 +65,10 @@ export default ({ return currencies[this.groupSettings.mincomeCurrency].displayWithCurrency }, humanDueDate () { - return this.humanDate(this.dueDateForPeriod(this.currentPaymentPeriod)) + return humanDate(this.dueDateForPeriod(this.currentPaymentPeriod)) }, humanStartDate () { - return this.humanDate(this.periodStampGivenDate(this.currentPaymentPeriod)) + return humanDate(this.periodStampGivenDate(this.currentPaymentPeriod)) }, summaryCopy () { const { paymentsTotal, paymentsDone, hasPartials, amountTotal, amountDone } = this.ourPaymentsSummary From f5c684affac221a4d55a76de20252c59ad95c7ec Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sat, 23 Sep 2023 23:18:36 +0200 Subject: [PATCH 30/36] Use payment.period to fix Invalid Date errors --- .../views/containers/payments/PaymentRowReceived.vue | 10 +--------- .../views/containers/payments/PaymentRowSent.vue | 12 ++---------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/frontend/views/containers/payments/PaymentRowReceived.vue b/frontend/views/containers/payments/PaymentRowReceived.vue index 51c67c3bb..52685af3e 100644 --- a/frontend/views/containers/payments/PaymentRowReceived.vue +++ b/frontend/views/containers/payments/PaymentRowReceived.vue @@ -21,7 +21,7 @@ .cpr-date.has-text-1 {{ humanDate(payment.date) }} template(slot='cellRelativeTo') - .c-relative-to.has-text-1 {{ humanDate(payment.relativeTo) }} + .c-relative-to.has-text-1 {{ humanDate(payment.period) }} template(slot='cellActions') payment-actions-menu @@ -73,20 +73,12 @@ export default ({ PaymentRow }, mixins: [PaymentsMixin], - data () { - return { - relativeTo: null - } - }, props: { payment: { type: Object, required: true } }, - async mounted () { - this.relativeTo = await this.historicalPeriodStampGivenDate(this.payment.date) - }, computed: { ...mapGetters([ 'withGroupCurrency' diff --git a/frontend/views/containers/payments/PaymentRowSent.vue b/frontend/views/containers/payments/PaymentRowSent.vue index 7c0723468..55d1166d5 100644 --- a/frontend/views/containers/payments/PaymentRowSent.vue +++ b/frontend/views/containers/payments/PaymentRowSent.vue @@ -21,7 +21,7 @@ .cpr-date.has-text-1 {{ humanDate(payment.date) }} template(slot='cellRelativeTo') - .c-relative-to.has-text-1 {{ humanDate(payment.relativeTo) }} + .c-relative-to.has-text-1 {{ humanDate(payment.period) }} template(slot='cellActions') payment-actions-menu @@ -65,11 +65,6 @@ export default ({ PaymentNotReceivedTooltip, PaymentRow }, - data () { - return { - relativeTo: null - } - }, mixins: [PaymentsMixin], props: { payment: { @@ -77,9 +72,6 @@ export default ({ required: true } }, - async mounted () { - this.relativeTo = await this.historicalPeriodStampGivenDate(this.payment.date) - }, computed: { ...mapGetters([ 'ourGroupProfile', @@ -89,7 +81,7 @@ export default ({ return this.payment.data.status === PAYMENT_NOT_RECEIVED }, isOldPayment () { - // check if it's a past transaction item. + // Check if the payment is relative to an older period. return comparePeriodStamps(this.payment.period, this.currentPaymentPeriod) < 0 } }, From 0d84402f280d210b1d9a09dcb63c0ef760e87eda Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 24 Sep 2023 10:56:15 +0200 Subject: [PATCH 31/36] Add .start field in initPaymentPeriod --- frontend/model/contracts/group.js | 5 +++-- frontend/model/contracts/manifests.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index a580782d7..997390323 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -42,8 +42,9 @@ function initGroupProfile (contractID: string, joinedDate: string) { } } -function initPaymentPeriod ({ getters }) { +function initPaymentPeriod ({ meta, getters }) { return { + start: getters.periodStampGivenDate(meta.createdDate), // this saved so that it can be used when creating a new payment initialCurrency: getters.groupMincomeCurrency, // TODO: should we also save the first period's currency exchange rate..? @@ -85,7 +86,7 @@ function clearOldPayments ({ contractID, state, getters }) { function initFetchPeriodPayments ({ contractID, meta, state, getters }) { const period = getters.periodStampGivenDate(meta.createdDate) - const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters })) + const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ meta, getters })) clearOldPayments({ contractID, state, getters }) return periodPayments } diff --git a/frontend/model/contracts/manifests.json b/frontend/model/contracts/manifests.json index 5813c41e5..e9fe1b06b 100644 --- a/frontend/model/contracts/manifests.json +++ b/frontend/model/contracts/manifests.json @@ -1,7 +1,7 @@ { "manifests": { "gi.contracts/chatroom": "21XWnNW8iGnrFhGVr7Yn2oipnhEMfLSjkKnDUk2tztJTHSP5u2", - "gi.contracts/group": "21XWnNTu2JzqBLksJQw7eL3cTZhFCuxTVHVWGHovbao444p9aR", + "gi.contracts/group": "21XWnNJDR4gWCBm6XfG1uumvxjCALZbook7pvhHWKDfAXV9Vx2", "gi.contracts/identity": "21XWnNFMAQ4x2GyiJRUdQjaVLhRYLwag4D5E4gYze2KtboKDXw", "gi.contracts/mailbox": "21XWnNQGtE5jZa2p8LKL2XS6HPrUf2S4R8nkNcTancSp47U9sK" } From 4368f657137b29ed0f083782af77daabd1d8c432 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 24 Sep 2023 11:42:57 +0200 Subject: [PATCH 32/36] Add .end field in in-memory payment periods --- frontend/model/contracts/group.js | 11 +++++++++-- frontend/model/contracts/manifests.json | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 997390323..7c857536b 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -14,7 +14,7 @@ import { import { paymentStatusType, paymentType, PAYMENT_COMPLETED } from './shared/payments/index.js' import { createPaymentInfo, paymentHashesFromPaymentPeriod } from './shared/functions.js' import { merge, deepEqualJSONType, omit, cloneDeep } from './shared/giLodash.js' -import { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, dateIsWithinPeriod, DAYS_MILLIS, periodStampsForDate } from './shared/time.js' +import { addTimeToDate, dateToPeriodStamp, compareISOTimestamps, dateFromPeriodStamp, isPeriodStamp, comparePeriodStamps, dateIsWithinPeriod, DAYS_MILLIS, periodStampsForDate, plusOnePeriodLength } from './shared/time.js' import { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js' import currencies from './shared/currencies.js' import { inviteType, chatRoomAttributesType } from './shared/types.js' @@ -43,8 +43,10 @@ function initGroupProfile (contractID: string, joinedDate: string) { } function initPaymentPeriod ({ meta, getters }) { + const start = getters.periodStampGivenDate(meta.createdDate) return { - start: getters.periodStampGivenDate(meta.createdDate), + start, + end: plusOnePeriodLength(start, getters.groupSettings.distributionPeriodLength), // this saved so that it can be used when creating a new payment initialCurrency: getters.groupMincomeCurrency, // TODO: should we also save the first period's currency exchange rate..? @@ -87,6 +89,11 @@ function clearOldPayments ({ contractID, state, getters }) { function initFetchPeriodPayments ({ contractID, meta, state, getters }) { const period = getters.periodStampGivenDate(meta.createdDate) const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ meta, getters })) + const previousPeriod = getters.periodBeforePeriod(period) + // Update the '.end' field of the previous in-memory period, if any. + if (previousPeriod in state.paymentsByPeriod) { + state.paymentsByPeriod[previousPeriod].end = period + } clearOldPayments({ contractID, state, getters }) return periodPayments } diff --git a/frontend/model/contracts/manifests.json b/frontend/model/contracts/manifests.json index e9fe1b06b..a4e188d91 100644 --- a/frontend/model/contracts/manifests.json +++ b/frontend/model/contracts/manifests.json @@ -1,7 +1,7 @@ { "manifests": { "gi.contracts/chatroom": "21XWnNW8iGnrFhGVr7Yn2oipnhEMfLSjkKnDUk2tztJTHSP5u2", - "gi.contracts/group": "21XWnNJDR4gWCBm6XfG1uumvxjCALZbook7pvhHWKDfAXV9Vx2", + "gi.contracts/group": "21XWnNKgXo9vJB7BrX1ScGuKNVF6mnrnenBLbFFSM34oFzrJeX", "gi.contracts/identity": "21XWnNFMAQ4x2GyiJRUdQjaVLhRYLwag4D5E4gYze2KtboKDXw", "gi.contracts/mailbox": "21XWnNQGtE5jZa2p8LKL2XS6HPrUf2S4R8nkNcTancSp47U9sK" } From 43b070be0ea9ca750f27b1e34c0728f473a1610a Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:14:09 +0200 Subject: [PATCH 33/36] Rename getPeriodPayment to getPaymentPeriod --- frontend/views/containers/contributions/TodoHistory.vue | 2 +- frontend/views/containers/payments/PaymentsMixin.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/views/containers/contributions/TodoHistory.vue b/frontend/views/containers/contributions/TodoHistory.vue index 2269a4435..9035bed75 100644 --- a/frontend/views/containers/contributions/TodoHistory.vue +++ b/frontend/views/containers/contributions/TodoHistory.vue @@ -56,7 +56,7 @@ export default ({ if (!period || comparePeriodStamps(period, firstDistributionPeriod) < 0) break const paymentDetails = await this.getPaymentDetailsByPeriod(period) - const { lastAdjustedDistribution } = await this.getPeriodPayment(period) + const { lastAdjustedDistribution } = await this.getPaymentPeriod(period) const doneCount = getLen(paymentDetails) const missedCount = getLen(lastAdjustedDistribution || {}) this.history.unshift({ diff --git a/frontend/views/containers/payments/PaymentsMixin.js b/frontend/views/containers/payments/PaymentsMixin.js index 2239d1f0f..72863eafb 100644 --- a/frontend/views/containers/payments/PaymentsMixin.js +++ b/frontend/views/containers/payments/PaymentsMixin.js @@ -169,7 +169,7 @@ const PaymentsMixin: Object = { // Returns the stored payment period object for a given period stamp, // or an empty object if not found. // TODOs: rename to getPaymentPeriod, and maybe avoid loading all historical payment periods. - async getPeriodPayment (period: string) { + async getPaymentPeriod (period: string) { if (Object.keys(this.groupPeriodPayments).includes(period)) { return this.groupPeriodPayments[period] || {} } From 8f2519d18b37830021d99522b336611eab162d66 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Sun, 1 Oct 2023 12:31:50 +0200 Subject: [PATCH 34/36] Fix issue 1739 --- .../containers/payments/PaymentsMixin.js | 53 +++++++++---------- frontend/views/pages/Payments.vue | 2 +- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/frontend/views/containers/payments/PaymentsMixin.js b/frontend/views/containers/payments/PaymentsMixin.js index 72863eafb..67b81966f 100644 --- a/frontend/views/containers/payments/PaymentsMixin.js +++ b/frontend/views/containers/payments/PaymentsMixin.js @@ -29,6 +29,10 @@ const PaymentsMixin: Object = { return await this.historicalPeriodStampGivenDate(payment.date) }, + async getAllPaymentPeriods () { + return { ...await this.getHistoricalPaymentPeriods(), ...this.groupPeriodPayments } + }, + // Oldest key first. async getAllSortedPeriodKeys () { const historicalPaymentPeriods = await this.getHistoricalPaymentPeriods() @@ -69,44 +73,40 @@ const PaymentsMixin: Object = { return await sbp('gi.db/archive/load', ourArchiveKey) ?? {} }, - async getHistoricalPaymentsInTypes () { - const paymentsInTypes = { - sent: cloneDeep(this.ourPayments?.sent || []), - received: cloneDeep(this.ourPayments?.received || []), - todo: cloneDeep(this.ourPayments?.todo || []) - } - const paymentsByPeriod = await this.getHistoricalPaymentPeriods() + async getAllPaymentsInTypes () { + const sent = [] + const received = [] + const todo = cloneDeep(this.ourPayments?.todo ?? []) + const paymentPeriods = await this.getAllPaymentPeriods() + const sortPayments = (f, l) => f.meta.createdDate > l.meta.createdDate ? 1 : -1 - for (const period of Object.keys(paymentsByPeriod).sort().reverse()) { - const paymentsKey = `payments/${this.ourUsername}/${period}/${this.currentGroupId}` - const payments = await sbp('gi.db/archive/load', paymentsKey) || {} - const { paymentsFrom } = paymentsByPeriod[period] + for (const periodStamp of Object.keys(paymentPeriods).sort().reverse()) { + const paymentsByHash = await this.getPaymentDetailsByPeriod(periodStamp) + const { paymentsFrom } = paymentPeriods[periodStamp] for (const fromUser of Object.keys(paymentsFrom)) { for (const toUser of Object.keys(paymentsFrom[fromUser])) { if (toUser === this.ourUsername || fromUser === this.ourUsername) { - const receivedOrSent = toUser === this.ourUsername ? 'received' : 'sent' + const receivedOrSent = toUser === this.ourUsername ? received : sent for (const hash of paymentsFrom[fromUser][toUser]) { - if (hash in payments) { - const { data, meta } = payments[hash] - paymentsInTypes[receivedOrSent].push({ hash, data, meta, amount: data.amount, username: toUser, period }) + if (hash in paymentsByHash) { + const { data, meta } = paymentsByHash[hash] + receivedOrSent.push({ hash, data, meta, amount: data.amount, username: toUser, period: periodStamp }) } else { - console.error(`getHistoricalPaymentsInTypes: couldn't find payment ${hash} for period ${period}!`) + console.error(`getAllPaymentsInTypes: couldn't find payment ${hash} for period ${periodStamp}!`) } } } } } } - const sortPayments = payments => payments - .sort((f, l) => f.meta.createdDate > l.meta.createdDate ? 1 : -1) - paymentsInTypes.sent = sortPayments(paymentsInTypes.sent) - paymentsInTypes.received = sortPayments(paymentsInTypes.received) - - return paymentsInTypes + sent.sort(sortPayments) + received.sort(sortPayments) + return { received, sent, todo } }, + // Returns archived or in-memory stored data by payment hash for the given period. async getPaymentDetailsByPeriod (period: string) { let detailedPayments = {} - if (Object.keys(this.groupPeriodPayments).includes(period)) { + if (period in this.groupPeriodPayments) { const paymentHashes = this.paymentHashesForPeriod(period) || [] detailedPayments = Object.fromEntries(paymentHashes.map(hash => [hash, this.currentGroupState.payments[hash]])) } else { @@ -121,6 +121,7 @@ const PaymentsMixin: Object = { } return detailedPayments }, + // Returns a list of payment info objects for completed payments during the given period. async getPaymentsByPeriod (period: string) { const payments = [] const paymentsByHash = await this.getPaymentDetailsByPeriod(period) @@ -170,11 +171,7 @@ const PaymentsMixin: Object = { // or an empty object if not found. // TODOs: rename to getPaymentPeriod, and maybe avoid loading all historical payment periods. async getPaymentPeriod (period: string) { - if (Object.keys(this.groupPeriodPayments).includes(period)) { - return this.groupPeriodPayments[period] || {} - } - const archPaymentsByPeriod = await this.getHistoricalPaymentPeriods() - return archPaymentsByPeriod[period] || {} + return this.groupPeriodPayments[period] ?? (await this.getHistoricalPaymentPeriods())[period] ?? {} }, // Returns a human-readable description of the time interval identified by a given period stamp. getPeriodFromStartToDueDate (period) { diff --git a/frontend/views/pages/Payments.vue b/frontend/views/pages/Payments.vue index 0e9de0140..a8261382d 100644 --- a/frontend/views/pages/Payments.vue +++ b/frontend/views/pages/Payments.vue @@ -476,7 +476,7 @@ export default ({ async updatePayments () { // NOTE: no need to calculate while logging out if (Object.keys(this.groupSettings).length) { - this.historicalPayments = await this.getHistoricalPaymentsInTypes() + this.historicalPayments = await this.getAllPaymentsInTypes() } } } From b7767f541706e010b23771f2a4937287cf06ea10 Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Tue, 3 Oct 2023 19:18:03 +0200 Subject: [PATCH 35/36] Pin contracts --- contracts/0.1.8/group-slim.js | 11 +++++++++-- contracts/0.1.8/group.0.1.8.manifest.json | 2 +- contracts/0.1.8/group.js | 11 +++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/contracts/0.1.8/group-slim.js b/contracts/0.1.8/group-slim.js index a28e85b4e..50c39ff7c 100644 --- a/contracts/0.1.8/group-slim.js +++ b/contracts/0.1.8/group-slim.js @@ -889,8 +889,11 @@ ${this.getErrorInfo()}`; incomeDetailsLastUpdatedDate: null }; } - function initPaymentPeriod({ getters }) { + function initPaymentPeriod({ meta, getters }) { + const start = getters.periodStampGivenDate(meta.createdDate); return { + start, + end: plusOnePeriodLength(start, getters.groupSettings.distributionPeriodLength), initialCurrency: getters.groupMincomeCurrency, mincomeExchangeRate: 1, paymentsFrom: {}, @@ -914,7 +917,11 @@ ${this.getErrorInfo()}`; } function initFetchPeriodPayments({ contractID, meta, state, getters }) { const period = getters.periodStampGivenDate(meta.createdDate); - const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters })); + const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ meta, getters })); + const previousPeriod = getters.periodBeforePeriod(period); + if (previousPeriod in state.paymentsByPeriod) { + state.paymentsByPeriod[previousPeriod].end = period; + } clearOldPayments({ contractID, state, getters }); return periodPayments; } diff --git a/contracts/0.1.8/group.0.1.8.manifest.json b/contracts/0.1.8/group.0.1.8.manifest.json index 8599321cc..e2d032ae0 100644 --- a/contracts/0.1.8/group.0.1.8.manifest.json +++ b/contracts/0.1.8/group.0.1.8.manifest.json @@ -1 +1 @@ -{"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.1.8\",\"contract\":{\"hash\":\"21XWnNJcM1WfRQmw3kXXzWVNCYSCT9WWmZPvQR299XZnoZYn55\",\"file\":\"group.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"group-slim.js\",\"hash\":\"21XWnNFAPqkukffCayHhF27Z75EaWDz3vMVmtNRVNa5kNq6zQY\"}}","signature":{"key":"","signature":""}} \ No newline at end of file +{"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.1.8\",\"contract\":{\"hash\":\"21XWnNXfHboTVJK79wU3SvLKaCVv5BtqBzkv56FDAwnrMVzgdC\",\"file\":\"group.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"group-slim.js\",\"hash\":\"21XWnNLBiRkz2ZwKV24ktuiqSH6EwDAY3tNDUkfSpKkQWWSCNB\"}}","signature":{"key":"","signature":""}} \ No newline at end of file diff --git a/contracts/0.1.8/group.js b/contracts/0.1.8/group.js index 1abc14aaf..e860a5ef8 100644 --- a/contracts/0.1.8/group.js +++ b/contracts/0.1.8/group.js @@ -10013,8 +10013,11 @@ ${this.getErrorInfo()}`; incomeDetailsLastUpdatedDate: null }; } - function initPaymentPeriod({ getters }) { + function initPaymentPeriod({ meta, getters }) { + const start = getters.periodStampGivenDate(meta.createdDate); return { + start, + end: plusOnePeriodLength(start, getters.groupSettings.distributionPeriodLength), initialCurrency: getters.groupMincomeCurrency, mincomeExchangeRate: 1, paymentsFrom: {}, @@ -10038,7 +10041,11 @@ ${this.getErrorInfo()}`; } function initFetchPeriodPayments({ contractID, meta, state, getters }) { const period = getters.periodStampGivenDate(meta.createdDate); - const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ getters })); + const periodPayments = vueFetchInitKV(state.paymentsByPeriod, period, initPaymentPeriod({ meta, getters })); + const previousPeriod = getters.periodBeforePeriod(period); + if (previousPeriod in state.paymentsByPeriod) { + state.paymentsByPeriod[previousPeriod].end = period; + } clearOldPayments({ contractID, state, getters }); return periodPayments; } From d19ed2bbf5a97dd4d8c781e5d6e4b14dac7516bc Mon Sep 17 00:00:00 2001 From: snowteamer <64228468+snowteamer@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:51:58 +0200 Subject: [PATCH 36/36] Add test for payment in 2nd period --- test/cypress/integration/group-paying.spec.js | 93 ++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/test/cypress/integration/group-paying.spec.js b/test/cypress/integration/group-paying.spec.js index a65247b68..11c6a10c5 100644 --- a/test/cypress/integration/group-paying.spec.js +++ b/test/cypress/integration/group-paying.spec.js @@ -374,6 +374,97 @@ describe('Group Payments', () => { }) }) + it('user1 sends $250 to user3 (again)', () => { + cy.giSwitchUser(`user1-${userId}`, { bypassUI: true }) + + cy.giForceDistributionDateToNow() + + cy.getByDT('paymentsLink').click() + cy.get('[data-test-date]').should('have.attr', 'data-test-date', humanDateToday) + + assertNavTabs(['Todo1', 'Completed']) + assertMonthOverview([ + ['Payments sent', '0 out of 1'], + ['Amount sent', '$0 out of $250'] + ]) + cy.window().its('sbp').then(sbp => { + const { distributionPeriodLength } = sbp('state/vuex/getters').groupSettings + // Use 'Date.now()' here rather than 'timeStart' since a few seconds have already elapsed. + const start = humanDate(Date.now()) + const end = humanDate(Date.now() + distributionPeriodLength) + assertMonthOverviewTitle(`Period: ${start} - ${end}`) + }) + + cy.getByDT('recordPayment').should('be.disabled') + cy.getByDT('todoCheck').click({ force: true }) + cy.getByDT('recordPayment').should('not.be.disabled').click() + cy.getByDT('modal').within(() => { + cy.getByDT('payRecord').find('tbody').children().should('have.length', 1) + cy.getByDT('payRow').eq(0).find('input[data-test="amount"]').should('have.value', '250') + cy.getByDT('payRow').eq(0).find('label[data-test="check"]').click() + + cy.get('button[type="submit"]').click() + cy.getByDT('successClose').click() + cy.getByDT('closeModal').should('not.exist') + }) + + assertMonthOverview([ + ['Payments sent', '1 out of 1'], + ['Amount sent', '$250 out of $250'] + ]) + + cy.log('assert payments table is correct again') + assertNavTabs(['Todo', 'Completed']) + cy.getByDT('link-PaymentRowSent').click() + cy.getByDT('payList').find('tbody').children().should('have.length', 2) + cy.getByDT('payList').within(() => { + cy.getByDT('payRow').eq(0).find('td:nth-child(1)').should('contain', `user3-${userId}`) + cy.getByDT('payRow').eq(0).find('td:nth-child(2)').should('contain', '$250') + cy.getByDT('payRow').eq(0).find('td:nth-child(4)').should('contain', humanDateToday) + + cy.log('assert payment detail is correct') + cy.getByDT('menuTrigger').eq(0).click() + cy.getByDT('menuContent').find('ul > li:nth-child(1)').as('btnDetails') + cy.get('@btnDetails').should('contain', 'Payment details') + cy.get('@btnDetails').click() + }) + + cy.getByDT('modal').within(() => { + cy.getByDT('amount').should('contain', '$250') + cy.getByDT('subtitle').should('contain', `Sent to user3-${userId}`) + + cy.getByDT('details').find('li:nth-child(2)').should('contain', humanDate(timeStart, { month: 'long', year: 'numeric', day: 'numeric' })) + cy.getByDT('details').find('li:nth-child(3)').should('contain', '$1000') + }) + cy.closeModal() + + cy.log('user3 confirms the received payment again') + cy.giSwitchUser(`user3-${userId}`, { bypassUI: true }) + cy.getByDT('paymentsLink').click() + + cy.getByDT('payList').find('tbody').children().should('have.length', 2) + cy.getByDT('payList').within(() => { + cy.getByDT('payRow').eq(0).find('td:nth-child(1)').should('contain', `user1-${userId}`) + cy.getByDT('payRow').eq(0).find('td:nth-child(2)').should('contain', '$250') + cy.getByDT('payRow').eq(0).find('td:nth-child(4)').should('contain', humanDateToday) + }) + + assertMonthOverview([ + ['Payments received', '1 out of 1'], + ['Amount received', '$250 out of $250'] + ]) + + cy.log('user3 receives a notification for the payment and clicking on it opens a "Payment details" modal.') + openNotificationCard({ + messageToAssert: `user1-${userId} sent you a $250 mincome contribution. Review and send a thank you note.` + }) + + cy.getByDT('modal').within(() => { + cy.getByDT('modal-header-title').should('contain', 'Payment details') + }) + cy.closeModal() + }) + it('user1 changes their income details to "needing" and sees the correct UI', () => { cy.giSwitchUser(`user1-${userId}`, { bypassUI: true }) @@ -385,7 +476,7 @@ describe('Group Payments', () => { cy.getByDT('noPayments').should('exist') cy.getByDT('link-PaymentRowSent').click() - cy.getByDT('payList').find('tbody').children().should('have.length', 1) + cy.getByDT('payList').find('tbody').children().should('have.length', 2) assertMonthOverview([ ['Payments received', '0 out of 0'],