Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support variable distribution period length #1691

Merged
merged 38 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
03b63f8
Fix Vue error in groupProposalSettings
snowteamer Aug 14, 2023
3b9e246
Remove unused paymentTotalFromUserToUser getter
snowteamer Aug 14, 2023
b6c71c1
Add groupSortedPeriodKeys getter in group.js
snowteamer Aug 16, 2023
a922cb4
Update getters to support variable period length
snowteamer Aug 16, 2023
aa52565
Merge master into variable-period-length
snowteamer Aug 23, 2023
95b4838
Add isIsoString in time.js and use it
snowteamer Aug 24, 2023
7035686
Support variable period length in MonthOverview.vue
snowteamer Aug 28, 2023
11b4f90
Apply review
snowteamer Sep 13, 2023
46d8d70
Pin contracts to v0.1.8
snowteamer Sep 13, 2023
0361950
Add periodStampsForDate in time.js
snowteamer Sep 17, 2023
d455769
Add getHistoricalPaymentPeriods in PaymentMixin.js
snowteamer Sep 18, 2023
8a0e335
Rename getSortedPeriodKeys to getAllSortedPeriodKeys
snowteamer Sep 18, 2023
f9252c1
Add a few comments
snowteamer Sep 19, 2023
543311f
Export new helpers from time.js
snowteamer Sep 20, 2023
cb081c2
Improve validation in periodStampsForDate
snowteamer Sep 20, 2023
51568c3
Remove occurences of 'undefined'
snowteamer Sep 20, 2023
9162a5f
Fix missing break statement
snowteamer Sep 20, 2023
3dba4e2
Fix wrong getter name in SupportHistory.vue
snowteamer Sep 20, 2023
024ad27
fixup! Improve validation in periodStampsForDate
snowteamer Sep 20, 2023
8c239d5
Restore logging in Cypress
snowteamer Sep 20, 2023
5938968
Fix reactivity issue in MonthOverview.vue
snowteamer Sep 20, 2023
c1ec47a
Fix data() in PaymentRowReceived/Sent.vue
snowteamer Sep 20, 2023
f1c40c1
Use groupDistributionStarted i/o inWaitingPeriod
snowteamer Sep 20, 2023
2261521
Revert change in updateDistributionDate
snowteamer Sep 21, 2023
52ad851
Use humanStart/DueDate in MonthOverview.vue
snowteamer Sep 21, 2023
6e378e8
Simplify PaymentDetail.vue/initializeDetails
snowteamer Sep 21, 2023
0fc41eb
Use mounted i/o watch in PaymentRowReceived/Sent.vue
snowteamer Sep 21, 2023
2dd2102
Fix pedantic Flow error
snowteamer Sep 23, 2023
cd18f62
Pin contracts to 0.1.8
snowteamer Sep 23, 2023
44c5d3b
Move mounted() near the top
snowteamer Sep 23, 2023
7023b85
Revert humanDate to plain import in MonthOverview.vue
snowteamer Sep 23, 2023
f5c684a
Use payment.period to fix Invalid Date errors
snowteamer Sep 23, 2023
0d84402
Add .start field in initPaymentPeriod
snowteamer Sep 24, 2023
4368f65
Add .end field in in-memory payment periods
snowteamer Sep 24, 2023
43b070b
Rename getPeriodPayment to getPaymentPeriod
snowteamer Sep 29, 2023
8f2519d
Fix issue 1739
snowteamer Oct 1, 2023
b7767f5
Pin contracts
snowteamer Oct 3, 2023
d19ed2b
Add test for payment in 2nd period
snowteamer Oct 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 129 additions & 51 deletions frontend/model/contracts/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ 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, isIsoString, isPeriodStamp, comparePeriodStamps, 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'

Expand Down Expand Up @@ -312,32 +312,138 @@ 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()
taoeffect marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY: replace getters.currentGroupState.paymentsByPeriod ?? {} with getters.groupPeriodPayments which does the same thing.

},
// 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 (!isIsoString(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
taoeffect marked this conversation as resolved.
Show resolved Hide resolved
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) {
taoeffect marked this conversation as resolved.
Show resolved Hide resolved
// Only extrapolate one period length in the future.
const extrapolatedDistributionDate = addTimeToDate(
distributionDate, distributionPeriodLength
).toISOString()
if (recentDate >= extrapolatedDistributionDate) {
return dateToPeriodStamp(extrapolatedDistributionDate)
}
return dateToPeriodStamp(distributionDate)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should be replaced by the previous logic we had in the periodStampGivenDate function in time.js, so that it can extrapolate multiple periods out. There's no reason not to, and we have the code for that already.

I would recommend re-introducing that function in time.js, and passing in sortedPeriodKeys to it. That would keep this function here short & sweet while having the messy logic in time.js.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you're right, but I haven't seen any use case where extrapolating more than one period in the future would be actually useful. I thought it didn't make much sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say the user has their clock accidentally incorrectly set a year in the past. They will be browsing old payments they sent and using this getter to fetch the period in which the payment occurred. This getter will return the wrong value. Why should we return the wrong value when I've already written the code to return the correct value?

Copy link
Collaborator Author

@snowteamer snowteamer Sep 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is only called when recentDate >= distributionDate, and I don't think distributionDate can depend on the user's clock.

Therefore in case the clock incorrectly thinks it's already several period lengths in the future, way past the distribution date, so that this block gets called, then there just won't be any matching payment to be displayed but I think that's correct.

If the user has their clock accidentally incorrectly set a year in the past, so that recentDate is one year lower than expected, then this block won't be called. The sorted periods will be searched instead, and I think the user should still be able to browse any stored payment period up to the distribution date.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, ok, that does seem to be true. Still, I don't see why we can't include a loop here that was already written, and therefore not need to worry about this returning an incorrect value.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to handle future dates using the period length in a loop like before (re-using periodStampGivenDate in time.js)

}
// 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 >= dateFromPeriodStamp(waitingPeriodStamp).toISOString() ? waitingPeriodStamp : undefined
snowteamer marked this conversation as resolved.
Show resolved Hide resolved
}
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
snowteamer marked this conversation as resolved.
Show resolved Hide resolved
return latestKnownStamp
}
for (let i = 1; i < sortedPeriodKeys.length; i++) {
if (recentDate < sortedPeriodKeys) return sortedPeriodKeys[i - 1]
}
// This should not happen
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be if (recentDate < sortedPeriodKeys[i]) return sortedPeriodKeys[i - 1].

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, can't believe no test caught this - thanks

}
},
// 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
snowteamer marked this conversation as resolved.
Show resolved Hide resolved
if (!distributionDate) return
// This is not always the current period stamp.
const distributionDateStamp = dateToPeriodStamp(distributionDate)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need to do this conversion, because distributionDate is not a Date object. It is in fact a period stamp that you can use directly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought distributionDate was an ISO date string, which happens to also be a period stamp but is conceptually a different thing (as you explained to me). It made sense to me since distributionDate comes from the group settings, whereas "period stamp" is a more internal type I think.

So anywhere I had the chance to compare a distributionDate value against a date directly, I didn't do any conversion.
I will likely now have to add a few of such conversions there if I start to consider distributionDate as being of the "period stamp" type.

Copy link
Member

@taoeffect taoeffect Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snowteamer I think we made a mistake in how we named that variable... it is in fact a period stamp... and it happens to be a date represented as an ISO string.

Maybe you can add a comment to the constructor to that effect? (Renaming it now is not a good idea since there are live groups running).

Also, it's a bit confusing with how we've implemented dateToPeriodStamp, which takes either a Date or a string:

export function dateToPeriodStamp (date: string | Date): string {
  return new Date(date).toISOString()
}

And yet dateFromPeriodStamp only returns a Date:

export function dateFromPeriodStamp (daystamp: string): Date {
  return new Date(daystamp)
}

But yeah, in the end, distributionDate should be treated as a period stamp :-\

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for explaining! 😄

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
snowteamer marked this conversation as resolved.
Show resolved Hide resolved
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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand what is going on here or why this code is so complicated.

AFAICT this is what this function should be:

    periodBeforePeriod (state, getters) {
      return (periodStamp: string) => {
        const { distributionPeriodLength } = getters.groupSettings
        const sortedPeriods = getters.groupSortedPeriodKeys
        for (const i = sortedPeriods.length - 1; i >= 0; --i) {
          const latestPeriod = sortedPeriods[i]
          if (periodStamp >= latestPeriod) return latestPeriod
        }
        return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -distributionPeriodLength))
      }
    }

Copy link
Collaborator Author

@snowteamer snowteamer Sep 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is indeed a bit complicated, in part because of the periodStamp === distributionDateStamp case, so I've tried to explain it with a a lot of comments (but failed to make it clear enough)

this is what this function should be:

If I'm not mistaken, this won't ever return undefined to defer searching to the archive code. And since every archived period could have a different length, simply substracting distributionPeriodLength is not correct in that case.
Also it doesn't do anything special for the aforementioned special case.

Would be great if that case was actually not special and we could just use a simple loop like yours, but for now the suggested code fails this assertion line 118 in group-paying.spec.js

      expect(periodBeforePeriod(onePeriodLengthAhead) === distributionDate).to.be.true

(not 100% sure why but my guess it's because in that case there exists a stored object for the waiting period but not for the current distribution date yet)

Copy link
Member

@taoeffect taoeffect Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, that's a good point regarding distributionDate - my code doesn't check for that because it assumes (incorrectly) that that is already stored in sortedPeriods, which you're absolutely right, it might not be.

Also, my code has a bug related to this check:

if (periodStamp >= latestPeriod) return latestPeriod

I think that should be:

if (periodStamp > latestPeriod) return latestPeriod

The code that's currently here in the PR needs to be changed for a few reasons:

  1. As discussed above, we need to avoid passing distributionDate to dateToPeriodStamp
  2. It needs to be simplified similarly to what I wrote above. Right now it's got some very questionable lines, like this:
        if (periodStamp > distributionDateStamp) {
          // Only extrapolate one period length in the future.
          const onePeriodLenghtAhead = addTimeToDate(distributionDate, distributionPeriodLength)
          if (periodStamp === dateToPeriodStamp(onePeriodLenghtAhead)) return distributionDate
          else return
        }

This is very strange. The purpose of this function is to return the period before a given period stamp. That's not what the lines above do at all. Instead they seemingly randomly add a period length to distributionDate and compare periodStamp to that, and then bails if the check fails.

We need something conceptually equivalent to the code I wrote above that:

  1. Takes into account distributionDate
  2. Has a consistent mental model for how period stamps are created and iterated forward/backward

Mental model

Our mental model is as follows:

<--- [ UNKNOWN PAST ] --- [ SORTED_KEYS ] ---- [ UNKOWN FUTURE ] --->

Here we see 3 section. There is a "known" timeline in the center, and an unknown timeline at each end.

If we are given a period stamp that is inside of [ SORTED_KEYS ], here's what iterating it looks like:

[ A ] <-> [ B ] <-> [ C ]
            ^
            |
      periodStamp

If we want to go forward in time from periodStamp pointing at [ B ] stamp, then periodAfterPeriod points us to [ C ] and periodBeforePeriod points us to [ A ].

If, however, we have this situation:

[ B ] <-> [ C ] <-> [ UNKNOWN FUTURE ]
            ^
            |
      periodStamp

Then:

  • periodAfterPeriod adds distributionPeriodLength to periodStamp and returns the result
  • periodBeforePeriod returns [ B ]

And, if we have this situation:

[ UNKNOWN PAST ] <-> [ A ] <-> [ B ]
                       ^
                       |
                   periodStamp

Then:

  • periodAfterPeriod returns [ B ]
  • periodBeforePeriod returns undefined

Note: that this is the only scenario where undefined can be returned.

Also note: in that case historicalPeriodBeforePeriod (please rename from currently called getPeriodBeforePeriod) does the exact same logic as periodBeforePeriod, except it performs the check on a version of [ SORTED_KEYS ] that includes historical payments from then archive.

In fact, because both periodBeforePeriod and historicalPeriodBeforePeriod contain line-by-line exactly the same logic, please create a shared function inside of time.js that does this logic. When historicalPeriodBeforePeriod and periodBeforePeriod call that function, they will include an array containing the [ SORTED_KEYS ].

Finally, to take into account the distributionDate, simply add in the distributionDate to the [ SORTED_KEYS ] that you pass in to the internal periodBeforePeriod inside of time.js.

And similarly, there will be a corresponding shared periodAfterPeriod in time.js that is called from both group.js and PaymentsMixin.js.

Hope that's clear! Let me know if you have any questions.

Copy link
Collaborator Author

@snowteamer snowteamer Sep 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very strange. The purpose of this function is to return the period before a given period stamp.

Exactly, so if the given date is not actually a past, current or future period stamp but just a random ISO string, my assumption was that undefined had to be returned.

If that assumption shoud be dropped so that these getters behave like periodStampGivenDate, e.g. periodAfterPeriod returns B for any date between A and B not just for A, then maybe they could as well be renamed for consistency:

  • nextPeriodStampGivenDate
  • periodStampGivenDate
  • previousPeriodStampGivenDate
    (I'd rather use -forDate than -givenDate). Otherwise periodAfter/BeforeDate also sound good to me.

Another assumption in this PR, is that distributionPeriodLength is now a variable setting whose value only holds for the current period. It can no longer be safely used to iterate between existing periods, but is just a hint to initially setup a default next distribution date.
Moreover, in the waiting period the distribution date i.e. the next period stamp can be updated without updating distributionPeriodLength accordingly

}
},
// 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]
taoeffect marked this conversation as resolved.
Show resolved Hide resolved
}
},
dueDateForPeriod (state, getters) {
Expand All @@ -348,37 +454,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)
}
},
snowteamer marked this conversation as resolved.
Show resolved Hide resolved
paymentHashesForPeriod (state, getters) {
return (periodStamp) => {
const periodPayments = getters.groupPeriodPayments[periodStamp]
Expand Down Expand Up @@ -415,7 +490,7 @@ sbp('chelonia/defineContract', {
},
groupProposalSettings (state, getters) {
return (proposalType = PROPOSAL_GENERIC) => {
return getters.groupSettings.proposals[proposalType]
return getters.groupSettings.proposals?.[proposalType]
}
},
groupCurrency (state, getters) {
Expand Down Expand Up @@ -1262,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
snowteamer marked this conversation as resolved.
Show resolved Hide resolved
// Maybe we're updating the distribution date while in the waiting period.
if (inWaitingPeriod && meta.createdDate !== current) {
taoeffect marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
2 changes: 1 addition & 1 deletion frontend/model/contracts/manifests.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifests": {
"gi.contracts/chatroom": "21XWnNS9zeT2tws6KkJqkibLGfTivPdiGYbSjjqFEkDjL5Q775",
"gi.contracts/group": "21XWnNGw4mX4hBGLwq5TXPY1JWcBzEWXFqRhkWWdeBGvBCVhtj",
"gi.contracts/group": "21XWnNSdFZtCUSLtqdTnNeFLQdKFc2hWjsbYZkMZg3PjceNnZW",
"gi.contracts/identity": "21XWnNN7wGNxzFZmiS4ft2TumavYYAgemSuFrGavTVJpxqtnyg",
"gi.contracts/mailbox": "21XWnNHJ7MALCinR7vnu4GM82nq5DLhe6nhEDzruTuTw9CTnXP"
}
Expand Down
6 changes: 5 additions & 1 deletion frontend/model/contracts/shared/time.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,17 @@ export function humanDate (
}

export function isPeriodStamp (arg: string): boolean {
return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(arg)
return isIsoString(arg)
}

export function isFullMonthstamp (arg: string): boolean {
return /^\d{4}-(0[1-9]|1[0-2])$/.test(arg)
}

export function isIsoString (arg: string): boolean {
snowteamer marked this conversation as resolved.
Show resolved Hide resolved
return typeof arg === 'string' && /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(arg)
}

export function isMonthstamp (arg: string): boolean {
return isShortMonthstamp(arg) || isFullMonthstamp(arg)
}
Expand Down
24 changes: 4 additions & 20 deletions frontend/views/containers/contributions/SupportHistory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ div(:class='isReady ? "" : "c-ready"')

<script>
import { mapGetters } from 'vuex'
import { comparePeriodStamps } from '@model/contracts/shared/time.js'
import { MAX_HISTORY_PERIODS } from '@model/contracts/shared/constants.js'
import { L } from '@common/common.js'
import PaymentsMixin from '@containers/payments/PaymentsMixin.js'
import BarGraph from '@components/graphs/bar-graph/BarGraph.vue'
Expand All @@ -41,32 +39,18 @@ export default ({
computed: {
...mapGetters([
'currentPaymentPeriod',
'periodStampGivenDate',
'periodBeforePeriod',
'withGroupCurrency',
'groupTotalPledgeAmount',
'groupCreatedDate'
]),
firstDistributionPeriod () {
// group's first distribution period
return this.periodStampGivenDate(this.groupCreatedDate)
},
periods () {
const periods = [this.currentPaymentPeriod]
for (let i = 0; i < MAX_HISTORY_PERIODS - 1; i++) {
const period = this.periodBeforePeriod(periods[0])
if (comparePeriodStamps(period, this.firstDistributionPeriod) < 0) break
else periods.unshift(period)
}
return periods
}
])
},
mounted () {
this.updateHistory()
},
methods: {
async updateHistory () {
this.history = await Promise.all(this.periods.map(async (period, i) => {
const periods = await this.getSortedPeriodKeys()
Copy link
Member

@taoeffect taoeffect Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.getSortedPeriodKeys doesn't exist!

(Please test code before pushing to make sure it works. I might not always be a good enough of a reviewer to catch stuff like this, but the computer will always catch it if you test these cases)

Copy link
Collaborator Author

@snowteamer snowteamer Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I ran our test suite - I'm unfortunately no better at catching typos or old names, and need my tools to do it for me.😅

this.history = await Promise.all(periods.map(async (period, i) => {
const totalTodo = await this.getTotalTodoAmountForPeriod(period)
const totalDone = await this.getTotalPledgesDoneForPeriod(period)

Expand All @@ -82,7 +66,7 @@ export default ({
}
},
watch: {
periods () {
currentPaymentPeriod () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice update 👍

this.updateHistory()
}
}
Expand Down
17 changes: 5 additions & 12 deletions frontend/views/containers/contributions/TodoHistory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,8 @@ export default ({
computed: {
...mapGetters([
'currentPaymentPeriod',
'periodStampGivenDate',
'periodBeforePeriod',
'groupCreatedDate',
'dueDateForPeriod'
]),
firstDistributionPeriod () {
// group's first distribution period
return this.periodStampGivenDate(this.groupCreatedDate)
}
'groupCreatedDate'
])
},
created () {
this.updateHistory()
Expand All @@ -55,17 +48,17 @@ export default ({
this.history = []

let period = null
const firstDistributionPeriod = await this.getPeriodStampGivenDate(this.groupCreatedDate)
const getLen = obj => Object.keys(obj).length

for (let i = 0; i < MAX_HISTORY_PERIODS; i++) {
period = period === null ? this.currentPaymentPeriod : this.periodBeforePeriod(period)
if (comparePeriodStamps(period, this.firstDistributionPeriod) < 0) break
period = period === null ? this.currentPaymentPeriod : await this.getPeriodBeforePeriod(period)
if (!period || comparePeriodStamps(period, firstDistributionPeriod) < 0) break

const paymentDetails = await this.getPaymentDetailsByPeriod(period)
const { lastAdjustedDistribution } = await this.getPeriodPayment(period)
const doneCount = getLen(paymentDetails)
const missedCount = getLen(lastAdjustedDistribution || {})

this.history.unshift({
total: doneCount === 0 ? 0 : doneCount / (doneCount + missedCount),
title: this.getPeriodFromStartToDueDate(period),
Expand Down
18 changes: 13 additions & 5 deletions frontend/views/containers/payments/MonthOverview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
.c-summary(data-test='monthOverview')
i18n.c-summary-title.is-title-4(
tag='h4'
data-test='thisMonth'
:args='{ month: humanDate(Date.now(), { month: "long" }) }'
) {month} overview
data-test='monthOverviewTitle'
:args='{ start: humanDate(getStartDate()), end: humanDate(getDueDate()) }'
) Period: {start} - {end}

ul
li.c-summary-item(
Expand Down Expand Up @@ -36,14 +36,20 @@ import { mapGetters } from 'vuex'
import { PAYMENT_NOT_RECEIVED } from '@model/contracts/shared/payments/index.js'
import ProgressBar from '@components/graphs/Progress.vue'
import { L } from '@common/common.js'
import { humanDate } from '@model/contracts/shared/time.js'
import { addTimeToDate, humanDate } from '@model/contracts/shared/time.js'

export default ({
name: 'MonthOverview',
components: {
ProgressBar
},
methods: {
getDueDate () {
return addTimeToDate(this.getStartDate(), this.groupSettings.distributionPeriodLength)
},
taoeffect marked this conversation as resolved.
Show resolved Hide resolved
getStartDate () {
return this.periodStampGivenDate(new Date())
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this currentPaymentPeriod?

Copy link
Collaborator Author

@snowteamer snowteamer Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be but currentPaymentPeriod is a bit special in that it's controlled by reactiveDate, which doesn't update in real time but in interval. So if I understand correctly it will sometimes be wrong relative to the actual current date, therefore I chose to not rely on it in other computed properties

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snowteamer Alex is right, because Vue.js is not going to automatically update the UI when new Date() moves us into a new period. This is for 2 reasons:

  1. This is a method, not a computed property
  2. Even if it were a computed property, it still wouldn't work because computed properties are only updated in response to reactive changes. Since new Date() is not a reactive property itself, and is not updated as a result of a reactive Vue.js-related change (as reactiveDate is), the computed property doesn't get updated either because that code is never re-run

humanDate,
statusIsSent (user) {
return ['completed', 'pending'].includes(user.status)
Expand All @@ -54,10 +60,12 @@ export default ({
},
computed: {
...mapGetters([
'currentPaymentPeriod',
'ourGroupProfile',
'groupSettings',
'ourPaymentsSummary',
'ourPayments'
'ourPayments',
'periodStampGivenDate'
]),
currency () {
return currencies[this.groupSettings.mincomeCurrency].displayWithCurrency
Expand Down
Loading