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'],