Group-income-simple: Implement PayGroup page (V1)

Created on 6 Nov 2019  Β·  72Comments  Β·  Source: okTurtles/group-income-simple

Problem

The PayGroup page is unimplemented.

Solution

Implement the first version of the PayGroup page. The second version is being discussed here: #757

Implement having multiple frozen payments distributions (see comment in Payments.vue).

Closing this issue implies closing #206 (cash-based payments)


Update May: based on #900, here's what's still missing:

What doesn't work

  • [ ] Figuring out what to do when a payment is marked as not received (no proper followup to this) β€” @mmbotelho @dotmacro please play with it and see if you can offer suggestions
  • [ ] Proper display of currencies based on the group's currency setting
  • [ ] Conversion of currencies or handling currency change proposals
  • [ ] Payments list pagination
  • [x] Display and record Late payments (started at #918)
  • [ ] Partial payments that are done in more than 2 parts. (e.g. $100 payed 25 + 40 + 35)
  • [ ] Support fetching old payments (+1 month ago)

What needs to be done next

TODO:

  • [ ] Sorting and reverse sorting the payments list by clicking the tabs
  • [ ] Implement and use the prevMonthDueDate computed property for late payments in PaymentRowTodo.vue
  • [ ] Cancelling payments? Not sure how that flow is supposed to work, wasn't clear from Figma (cc @mmbotelho)
  • [ ] Maybe make it so that payments can be marked as completed immediately when paymentType === PAYMENT_TYPE_MANUAL (see TODO in /payment in group.js), although this one isn't that important, it works well enough as it is now without it (two messages sent, one /payment followed by a /paymentUpdate)
  • [ ] Making the payment date information use the time given by the server instead of the system clock (so no new Date() usage, but creating dates based on an API call, see #531 and the /time API defined on the backend)
  • [ ] Implement anything I forgot to mention from the "what doesn't work" section above

Done #951

  • [x] Making the ESC key clear the search field
  • [x] Fixing a bug where if we switched from sending payments to receiving payments, we suddenly lose the "Sent" tab
  • [x] Add tests to received payments and a scenario where a user changes from giving to receiving mincome

Done #917

  • [x] Someone needs to go through and clean up all of the CSS β€” deduplicate the all of the copy/pasted junk in PaymentRow*.vue files and DRY it all so that it's all stored in one of the .scss files and not repeated
  • [x] Create mixins and remove no longer used computed properties/methods from the PaymentRow*.vue files (and any other payment related files)
  • [x] Delete humanDate.js and move the humanDate function into time.js
  • [x] Delete outdated comments in the markup
  • [x] Cleanup and DRY the markup, CSS, and any JS, but especially the CSS

Done #918

  • [x] Correctly implement the PaymentDetails.vue file
  • [x] Sidebar overview of payments (the stats on the righthand side)
  • [x] Proper display of payment due dates and late payment dates
  • [x] Correctly implement the MonthOverview.vue file
  • [x] Remove the Math.random() stuff in the PaymentRow*.vue files (mostly related to payment date stuff) and replace it with the actual data
  • [x] Reset button in RecordPayments modal, and other functionality like the memo field (didn't hook that up, needs to be done, but should be straightforward now)
  • [x] Error handling!! I did not add any proper error handling or error displaying for sending payments, or for the input forms. Vuelidate validations need to be added where appropriate as well.
Frontend Core Payments UUX High

All 72 comments

This is not designed yet... It's a bit confusing to have this issue open already

@mmbotelho oh but this issue refers to the designs you've already made and that have already been implemented. My plan was to implement the logic in a way that should be compatible with the new designs you're working on (or at the very least doesn't require much change by way of the core contract logic), with what we already have implemented from you previous designs. Does that sound ok to you?

It does, yes, but what I'm trying to say is that we should discuss everything and do the mockups first before we move on to implementing any logic or UI. This way we prevent having miscommunications and implementing and designing things that don't match.

I believe @pieer already implemented this, right? Can we close it @taoeffect ?

I’m currently working on this issue. Pierre implemented the UI, the functionality is still unfinished.

ok @taoeffect!

I updated this issue description with what's missing from #900. I'll review the list and pick a small chunk to fix on a new PR. (I'll let you know which parts soon).
If I have questions, I'll leave them here.

-- EDIT --

I'll start with cleaning up PaymentRow*.vue components using named slots and avoiding v-if.

  • Making the payment date information use the time given by the server instead of the system clock (so no new Date() usage, but creating dates based on an API call, see #531 and the /time API defined on the backend)
    ...
  • Tests!! I did not add a single test for to this, and it will be challenging to test some things like late payments since that requires time manipulation of some kind

Currently, late payments can be manually tested by adjusting the system clock. Can we leave the system clock as an option at least until we have other working tests for late payments?

Currently, late payments can be manually tested by adjusting the system clock. Can we leave the system clock as an option at least until we have other working tests for late payments?

Oh, yeah, we will have special code I'm guessing for testing purposes to adjust the clock, not based on system time but based on whatever we need to do the tests.

As for the May 2020 update - I left answers to these questions on PR #900 but maybe it got buried, I'll copy paste the answers here:

Figuring out what to do when a payment is marked as not received (no proper followup to this) β€” @mmbotelho @dotmacro please play with it and see if you can offer suggestions

This flow is specified on Figma - Read the orange notes there for more info! If you still have questions, let me know!

Cancelling payments? Not sure how that flow is supposed to work, wasn't clear from Figma (cc @mmbotelho)

For manual payments, the payment is no longer β€œdone”, therefore it returns to its original state in the todo page. It’s basically an undo.

@taoeffect, technical question:
While I'm implementing the logic for PaymentDetail.vue (the modal), I realized that the monthstamp should also include the year. E.g 04 (May) should be 2020-04.
This is needed in the UI where it says "_Relative to_" and also to be future proof through the years. (ex: December 2019 to Jan 2020)

@sandrina-p The monthstamp already does include the year...?

@taoeffect, apologize me, for some reason when I checked I got the idea it was not there, but it is. Nevermind! πŸ€¦β€β™€οΈ πŸ€¦β€β™€οΈ

While playing with payments, I noticed multiple scenarios with unexpected outcomes regarding payments distribution:

Chunk A: When someone updates their income details after a payment is already made:

Scenario 1:

  1. Create a group with $1000 mincome and 3 members:

    • u1: pledge 100$

    • u2: Income 925$ (a.k.a needs $75)

    • u3: no income details added.

  2. Login u1 and send $75 to u2. - The payment goes as expected.
  3. Switch to u3 and add income details: Income 950$ (a.k.a needs $50)

    • Result: It shows "You'll receive $40" in the graphic summary.

    • Expected: Should receive only $25 from u1 (100 - 75)

  4. Switch to u1 and go to the payments page.

    • Result: Shows 1 payment to u3 of $40. The sidebar says "Amount sent $75 out of $115"

    • Expected: It should show 1 payment to u3 of $25. The sidebar should say "Amount sent $75 of $100"

Scenario 2:

  1. Create a group with $1000 mincome and 3 members:

    • u1: pledge 100$

    • u2: Income 900$ (a.k.a needs $100)

    • u3: no income details added.

  2. Login u1 and send $100 to u2. - The payment goes as expected.
  3. Switch to u3 and add income details: Income 950$ (a.k.a needs $50)

    • Result: It shows "You'll receive $33.333" in the graphic summary.

    • Expected: Should not receive anything from u1 because u1 already pledge all their money to u2.

  4. Switch to u1 and go to the payments page.

    • Result: Shows 1 payment to u3 of $33.33. The sidebar says "Amount sent $100 out of $133"

    • Expected: Don't show any "todo" payments. The sidebar should say "Amount sent $100 of $100"

Scenario 3:

(EDIT by Greg: Steven noticed an issue with Scenario 3, please read this comment)

  1. Create a group with $1000 mincome and 3 members:

    • u1: pledge 100$

    • u2: Income 950$ (a.k.a needs $50)

    • u3: no income details added.

  2. Login u1 and send $25 to u2 (a partial payment). - The payment goes as expected.
  3. Switch to u3 and add income details: Income 700$ (a.k.a needs $300)

    • Result: It shows "You'll receive $85.71" in the graphic summary.

    • Expected: It shows "You'll receive $75" in the graphic summary. Why? It's the result of 85.71 - (25 - 14.29). The u2 now should only receive $14.29 instead of the needed $50. But u1 already sent $25, so the difference should be discounted from $85.71.

  4. Switch to u1 and go to the payments page.

    • Result: Shows 1 "todo" payment to u3 of $85.71. The sidebar says "Amount sent $25 out of $110.71"

    • Expected: Shows 1 "todo" payment to u3 of $75. The sidebar says "Amount sent $25 out of $100"

Scenario 4:

  1. Create a group with $1000 mincome and 3 members:

    • u1: pledge 100$

    • u2: income 950$ (a.k.a needs $50)

    • u3: income 900$ (a.k.a needs $100)

  2. Login u1 and send $50 to u3 (a partial payment). - The payment goes as expected.
  3. Change the pledge amount from $100 to $50.

    • Result: Shows 1 "todo" payment to u2 of $16.67. The sidebar says "Amount sent $50 out of $66.67"

    • Expected: Don't show any payment to u3 because u1 already pledge all their money.

Scenario 4.1: (continuation)
  1. Invite a new member u4, who can pledge $150.

    • Result_1: u4 is asked to send 35.70 to u2 and 75 to u3.

    • Expected_1: u4 should be asked to send $50 to u2 and $50 to u3.

    • Result_2: u3's payments page says "Amount received $50 out of $125".

    • Expected_2: u3's payments page should say "Amount received $50 out of $100".

    • Result_3: u1 has a payment to do to u2 of $12.50

    • Expected_3: u1 should have no "todo" payments because u1 already pledge all their money.

A lot of similar "buggy" scenarios can be found... I didn't test when someone leaves the group or how re-distribution works regarding late payments but I'd assume it's also buggy.


Chunk B: Changing group mincome

I also found other bugs that happen when the group's mincome is changed. The income details of each user may not make sense anymore and lead to bugs. For example:

Scenario 1:

  1. Create a group with $1000 mincome and 2 members:

    • u1: pledge 100$

    • u2: Income 950$ (a.k.a needs $50)

  2. Change the mincome from $1000 to $500

    • Result: u1 still pledges 100$ and u2 needs "$-450" (nonsense value)

    • Expected: I don't know... we never discussed this.

Scenario 2:

  1. Create a group with $500 mincome and 2 members:

    • u1: pledge 100$

    • u2: Income 450$ (a.k.a needs $50)

  2. Change the mincome from $500 to $750

    • Result: u1 still pledges 100$ and u2 needs "$300"

    • Expected: Same, no idea. Maybe clear everyone's mincome and ask to fill it again? Or automatically update everyone's income details based on the new mincome and ask the user to double-check it?

Here are my thoughts on this:
In the first 3 scenarios, the problem seems to be that that whenever someone new adds their income details or someone changes their income details, the distribution is re-calculated but payments already sent in that month are not accounted for. I can think of 2 solutions for this:

  • Changes to the income details will only take place in the next month;
  • Make the distribution algorithm account for payments that were already sent.

Now, the last 2 scenarios. Scenario 1 group b (πŸ˜…):

Let's think about how we calculate how much money someone needs. We subtract their income from the mincome, correct? In this case, 500-950 is in fact negative $450. In human speak, however, what's happening here is that u2 now has $450 more than the mincome of the group. I believe we should treat it as the person no longer needs to receive a mincome, but because they have not added a pledge, they are a user that is "sending" mincome, but has a $0 pledge.

Scenario 1 group b (πŸ˜…):
u1 is still pledging $100 and will only meet 1/3 of the new u2 needs

Making a distribution algorithm that accounts for payments already sent (or accounting new members, or members that leave) seems complex, especially for the prototype... but @taoeffect can better decide that.

I'd agree that changes to the new income details should only take place in the next month, but that does not make sense when new members join the group and want to pledge immediately in that month. It's not very UX friendly...

(EDIT): I'm glad we have a "Dismiss payment" button. That can "solve" many of the incorrect payment values.

Regarding the chunk b of scenarios...

u1 is still pledging $100 and will only meet 1/3 of the new u2 needs

Not exactly. It depends a lot.

  • In the "mathematical approach" u1 wouldn't pledge anymore. This assuming that their income was $600 (500 + 100). So, now u1 would need $150 (750-600), but this might not be the truth...
  • IRL alternative A, u1 may have a huge income (let's say $10k) and doesn't mind to actually increase even more their pledging amount to support a bigger group mincome.
  • IRL alternative B, u1 has already an income of $900 but now with a group mincome of $750, they don't feel comfortable in pledging to the group anymore.

I think we shouldn't change automatically each member's income details because we don't know enough about them and each personal financial life is unique regardless of how much a member pledges/needs. TLDR: In my opinion, the safer option is to reset the income details or mark them as "stale" and ask the user to verify/update it. And only then, when all members had verified their income details, a new group distribution can be generated.

(EDIT): Another idea for that: (not sure if good though) When the user votes in a mincome proposal, ask them to update their income details, just in case the proposal gets approved. This way we don't need to guess new incomes details and we don't need to notify/prompt the user later to update their income details.

In my opinion, the safer option is to reset the income details or mark them as "stale" and ask the user to verify/update it. And only then, when all members had verified their income details, a new group distribution can be generated.

This sentence contradicts the sentence that came immediately before it.

Let's break this down.

Imagine a group that has a $1000 mincome. The group passes a proposal to change the mincome to $500. What is expected to happen is for pledges to stay the same and for users who were receiving contributions before BUT noww are abvove the mincome threshold, no longer receive mincome.

Resetting everyone's income details is not a good solution at all, because our app should be smart enough to calculate what should happen without harming anyone, and not force people to come fill out probably the same information they entered before. That would be very frustrating.

I see... When the mincome is decreased, what you are saying @mmbotelho, makes total sense. But if the mincome is increased, the pledging amounts may or may not make sense anymore and that's the tricky part.

I managed to get in some time to investigate Scenario 1 above. My investigation is incomplete, and I will continue it, but I will jot down my notes here:

There are two issues:

  1. The "Amount sent" shows $75 of $115 instead of $75 of $100. The reason for this is because of the following calculation:

    amountTotal: todo.reduce((total, p) => total + p.amount, 0) + sent.reduce((total, p) => total + p.data.amount, 0)
    

    The main issue being that the value for todo is "faulty" there, as in it includes a list item for u3 who added their income details later, and doesn't take into account the fact that we have no more $ left to send them. This can be fixed either by fixing the value returned in todo, or by using a Math.min operation. The more "correct" way to fix this, I am guessing, is to have todo contain the correct values that take into account that we cannot at this time send any $ to u3, and fixing it that way may fix many of the other issues you noticed.

  2. The TODO list contains an entry for $40 to u3 when it should contain an entry for $25 to u3 instead. I'm pretty sure this is a problem with how the groupIncomeDistribution calculates the adjusted distribution, i.e. here:

      if (adjusted) {
        // if this user has already made some payments to other users this
        // month, we need to take that into account and adjust the distribution.
        // this will be used by the Payments page to tell how much still
        // needs to be paid (if it was a partial payment).
        for (const p of dist) {
          const alreadyPaid = getters.paymentTotalFromUserToUser(p.from, p.to, monthstamp)
          // if we "overpaid" because we sent late payments, remove us from consideration
          p.amount = saferFloat(Math.max(0, p.amount - alreadyPaid))
        }
        dist = dist.filter(p => p.amount > 0)
      }
    

    Note that it's adjusting for whether u1 payed u2 previously, and not adjusting for u3 at all. So this math needs to be fixed.

Am getting closer to fixing it. Now the only thing left is to not make it a partial payment:

Screen Shot 2020-07-14 at 7 50 56 AM

  var dist = incomeDistribution(currentIncomeDistribution, mincomeAmount)
  if (!adjusted) {
    return dist
  } else {
    // if this user has already made some payments to other users this
    // month, we need to take that into account and adjust the distribution.
    // this will be used by the Payments page to tell how much still
    // needs to be paid (if it was a partial payment).
    const carried = {}
    const adjustedDist = []
    for (const p of dist) {
      const alreadyPaid = getters.paymentTotalFromUserToUser(p.from, p.to, monthstamp)
      const carryAmount = p.amount - alreadyPaid
      // if we "overpaid" because we sent late payments, remove us from consideration
      p.amount = saferFloat(Math.max(0, carryAmount))
      // calculate our carried adjustment (used when distribution changes due to new users)
      if (carried[p.from]) {
        carried[p.from].total += p.amount
      } else {
        carried[p.from] = { carry: 0, total: p.amount }
      }
      if (carryAmount < 0) {
        carried[p.from].carry += -carryAmount
      }
    }
    // we loop through and proportionally subtract the amount that we've already paid
    for (const p of dist) {
      if (p.amount > 0) {
        const c = carried[p.from]
        p.amount = saferFloat(p.amount - (c.carry * p.amount / c.total))
        adjustedDist.push(p)
      }
    }
    // console.debug('adjustedDist', adjustedDist, 'carried', carried)
    return adjustedDist
  }

This code should be equivalent to that but with a smaller diff from master:

  var dist = incomeDistribution(currentIncomeDistribution, mincomeAmount)
  if (adjusted) {
    // if this user has already made some payments to other users this
    // month, we need to take that into account and adjust the distribution.
    // this will be used by the Payments page to tell how much still
    // needs to be paid (if it was a partial payment).
    const carried = Object.create(null)
    for (const p of dist) {
      const alreadyPaid = getters.paymentTotalFromUserToUser(p.from, p.to, monthstamp)
      const carryAmount = p.amount - alreadyPaid
      // ex: it wants us to pay $2, but we already paid $3, thus: carryAmount = -$1 (all done paying)
      // ex: it wants us to pay $3, but we already paid $2, thus: carryAmount = $1 (remaining to pay)
      // if we "overpaid" because we sent late payments, remove us from consideration
      p.amount = saferFloat(Math.max(0, carryAmount))
      // calculate our carried adjustment (used when distribution changes due to new users)
      if (!carried[p.from]) carried[p.from] = { carry: 0, total: 0 }
      carried[p.from].total += p.amount
      if (carryAmount < 0) carried[p.from].carry += -carryAmount
    }
    // we loop through and proportionally subtract the amount that we've already paid
    dist = dist.filter(p => p.amount > 0)
    for (const p of dist) {
      const c = carried[p.from]
      p.amount = saferFloat(p.amount - (c.carry * p.amount / c.total))
    }
    // console.debug('adjustedDist', adjustedDist, 'carried', carried)
  }
  return dist

This is the other "more correct" solution that I tried:

// in groupIncomeDistribution.js

      const alreadyPaid = adjusted ? getters.paymentTotalFromUser(username, monthstamp) : 0
      currentIncomeDistribution.push({
        name: username,
        amount: saferFloat(amount - alreadyPaid)
      })
// in getters:

    // identical to paymentTotalFromUserToUser, except it adds up all payments to all users

    paymentTotalFromUser (state, getters) {
      return (fromUser, paymentMonthstamp) => {
        const payments = getters.currentGroupState.payments
        const monthlyPayments = getters.groupMonthlyPayments

        let total = 0

        const { paymentsFrom, mincomeExchangeRate } = monthlyPayments[paymentMonthstamp] || {}
        if (!paymentsFrom) return total

        const paymentsFromUser = paymentsFrom[fromUser]
        if (!paymentsFromUser) return total

        for (const paymentsToUser of Object.values(paymentsFromUser)) {
          for (const hash of paymentsToUser) {
            const payment = payments[hash]
            var { amount, exchangeRate, status } = payment.data
            if (status !== PAYMENT_COMPLETED) continue

            const paymentCreatedMonthstamp = ISOStringToMonthstamp(payment.meta.createdDate)
            // if this payment is from a previous month, 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 month's
            // exchange rates
            if (paymentMonthstamp !== paymentCreatedMonthstamp) {
              exchangeRate *= monthlyPayments[paymentCreatedMonthstamp].mincomeExchangeRate
            }
            total += amount * exchangeRate * mincomeExchangeRate
          }
        }

        return saferFloat(total)
      }
    },

The idea being, subtract what every user has already paid, from their "available" funds, before running their data through var dist = incomeDistribution(currentIncomeDistribution, mincomeAmount). Then, just let that function do all the distribution work as normal.

I think this is the more correct way to do it. However, I could have some mistaken assumptions about some of the semantics in the system. Either way, when I tried to verify manually that this had any affect on Scenario 1, I still saw the same results as before. This is what I meant in my last comment last week on Slack.

Trying to translate Scenario 1 to a Cypress test. Got this far, but it gets stuck after u1 adds his income details.

  // https://github.com/okTurtles/group-income-simple/issues/763#issuecomment-656250338
  it.only('scenario 1', () => {
    cy.visit('/')

    // Create a group with $1000 mincome and 3 members:
    cy.giSignup(`u1-${userId}`, { bypassUI: true })
    cy.giCreateGroup(groupName, { mincome: 1000, bypassUI: true })
    cy.giGetInvitationAnyone().then(url => {
      invitationLinks.anyone = url
    })

    // u1: pledge 100$
    setIncomeDetails(true, 100)
    cy.giLogout()

    // u2: Income 925$ (a.k.a needs $75)
    cy.giAcceptGroupInvite(invitationLinks.anyone, { username: `u2-${userId}`, groupName, bypassUI: true, shouldLogoutAfter: false })
    setIncomeDetails(false, 925)
    cy.giLogout()

    // u3: no income details added.
    cy.giAcceptGroupInvite(invitationLinks.anyone, { username: `u3-${userId}`, groupName, bypassUI: true, shouldLogoutAfter: false })

    // Login u1 and send $75 to u2. - The payment goes as expected.
    cy.giSwitchUser(`u1-${userId}`, { bypassUI: true })
    cy.getByDT('paymentsLink').click()
    cy.getByDT('recordPayment').click()
    cy.getByDT('modal').within(() => {
      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')
    })
  })

Fixed the aforementioned bug: the code inside the .then was running after the code which used the data it mutated. Putting the rest of the code inside the promise's callback fixed it.

Finished creating a test in 6a2e22f that mimics Scenario 1 as far as I understood it. (The text in line 315 didn't mention $40 as line 316 has but I hope that's correct, because that's what the test sees.)

The solution @taoeffect posted does solve this, but only when adjusted is true. In cae467a I had to change ourContributionSummary to access a version of groupIncomeDistribution that sets adjusted to true.

My only concern is that I'm not sure whether all views which use ourContributionSummary should have an adjusted view of this month's distribution. The only views which use it are:

  • ProfileCard: uses receivingMonetary to determine whether to show "Edit payment info" link
  • ContributionsWidget: this doesn't seem to be used, except in the "components" key of GroupDashboard, but I still can't figure out what that key is used for in Vue
  • Contributions: this is what we just solved

The "more correct" solution I listed above doesn't actually make the tests pass at all. It's a bit concerning to me because I thought it should actually resolve the issue, and more reliably (because it adjusts the data before sending it into existing machinery, instead of trying to adjust it after the data comes out), which shows that I'm missing something.

This passes all 3 tests in the Chunk A: When someone updates their income details after a payment is already made section.

The "Scenario 4.1: (continuation)" section fails. But it should probably be renamed to "Chunk 2: When someone is added to a group" or something, because that's why it fails.

I haven't yet got to Chunk B: Changing group mincome.

Regarding Chunk B: Scenario 1:

What should happen in this situation?

  1. group has mincome = 1000
  2. u1 pledges 100
  3. u2 needs 50
  4. group changes mincome to 500

IMO, neither setting should be changed:

  1. u1 can still pledge 100 safely, although they may want to increase it
  2. u2 is recorded as "making 950" in the system, which isn't untrue now, they simply don't pledge anything.

Instead, I suggest:

  1. u1 can probably remain pledging 100 safely, until they change it.
  2. the system simply calculate u2 as needing nothing (since 500 - 950 <= 0) and do all other calculations as normal

However, there's one thing to consider about this from a social perspective: They may have agreed to change the mincome because of a decrease in one or more members' incomes. Thus:

  1. If u1 decreased income (e.g. to 300), then the 100 pledge may not be safe anymore. They may need an opportunity to change this. Thus, we may want to "pause" their pledge, until they can verify or revise it.
  2. If u2 decreased income, they're already not pledging anything, so there's less "danger", but they may need/want to record this change in the system. Thus we may want to "ping" them and let them know they can change it, but continue to assume they can contribute nothing. However, their need may have increased, e.g. if they decreased to 300, then they need 200 now, not 50.

Overall, because of the above thoughts, I think that if the group votes to change the mincome, we should immediately pause all activity in the system and "invalidate" everyone's status (without erasing it in the system), so that no actions happen based on it, until they have a chance to look at it and revise their info.

What should happen in this situation?

So regardless of what happens u2 must have its settings switched from needing income to pledging something.

So the code to do that must be implemented, and the only question remaining is what amount to set them as pledging by default. This too is fairly easily answered by observing the following: u1's pledge is not being altered, it remains at 100. We therefore cannot switch u2 to pledging more than them, which is what would happen if we converted the excess amount above 500 into a pledge. Therefore we will simply set them as pledging 0.

And what happens if there is a third member in the group, u3, who needs 150 after mincome changes, but u1's pledge doesn't supply that fully (150 - 100 = 50 left)? Should u2 be bumped up to pledging 50 automatically?

No. In this scenario if there are any members who still need mincome, they will need less to help them achieve the new mincome. We never increase anyone's pledge. We only switch a person from needing mincome to not needing mincome (pledging 0), and inform them that they are now welcome and encouraged to increase their pledge if they can.

It looks like the code fix above did not actually fix the failing scenarios. The code behaves the same on all of them with or without it. It was actually the code in state.js in cae467a that fixed them. So we may be able to revert the code fix above. Unless it fixes another scenario, but I no longer remember how it works or what it does.

Scenario 3:

  1. Create a group with $1000 mincome and 3 members:

    • u1: pledge 100$

    • u2: Income 950$ (a.k.a needs $50)

    • u3: no income details added.

  2. Login u1 and send $25 to u2 (a partial payment). - The payment goes as expected.
  3. Switch to u3 and add income details: Income 700$ (a.k.a needs $300)

    • Result: It shows "You'll receive $85.71" in the graphic summary.

    • Expected: It shows "You'll receive $75" in the graphic summary. Why? It's the result of 85.71 - (25 - 14.29). The u2 now should only receive $14.29 instead of the needed $50. But u1 already sent $25, so the difference should be discounted from $85.71.

Over in Slack @sdegutis noticed correctly that this scenario isn't quite right, since after u3 enters their income details, there will be $75 left over that now needs to be split between u2 and u3, and so u3 cannot be expected to receive the full $75 that's remaining.

Instead, we likely need to run the distribution algorithm "again" over the remaining amounts needed between u2 and u3 (u2 needing $25, and u3 needing $300), so the remaining $75 will be split proportionally among those (and off the top of my head I don't know what that amounts to, but most of the $75 would go to u3, and some of it would go to u2).

Thanks to @sdegutis for noticing this (and @sandrina-p for coming up with these scenarios!).

Although my patch in #1000 fixes those scenarios in the unit tests, the Scenario 3 Cypress test is failing, in a strange way:

|Sender| Recipient | Unit Test | Cypress Test |
|-|-|-|-|
| u1 | u2 | $5.769230769230769 | $15 |
| u1 | u3 | $69.23076923076924 | $60 |

(The other Scenarios are passing in the Cypress tests.)

It doesn't make sense why the same function is having two different results. It seems there are more calculations being done here. This is an example of where I would reach for a debugger to step through the code and find out what's happening.

Besides that, at least two other Cypress tests are now also failing:

  • user1 sends $71.43 to user2 (total)
  • user1 sends $100 to user3 (partial)
  • (and probably more)

They have different amounts of payments with different amounts, and they show "-1 out of 2" for payments sent.

I'll translate them into the unit tests, to see if we get a similar difference as the table above.

Based on my understanding of the proportional-split algorithm:

  • Add up all the need, which in Scenario 3 is $325 ($25 for u2 + $300 for u3)
  • Divide the excess (u1's $75) based on each person's relative need based on the absolute need

    • u2 needs $25 out of $325 total need

    • which is 0.076923076923077 of the need

    • times the $75 excess = $5.769230769230769

    • u3 needs $300 out of $325 total need

    • which is 0.923076923076923 of the need

    • times the $75 excess = $69.230769230769231

Based on this understanding, correct or not, the unit tests are correct, and the Cypress tests show the wrong result.

I'm not sure yet how the Cypress tests came up with this answer.

@sdegutis Have you looked at how the Vue-path connects to the lower level code? I know off-hand that it goes through some getters like ourPayments, ourPaymentsSummary and ourContributionSummary. That might be where the differences are being generated.

Figured it out. The Cypress test for Scenario 3 was just written wrong. It had $900 instead of $950. So the algorithm works fine and the code is fixed.

The problem with "Payments Sent: -1 out of 2" is caused by it saying there is 1 sent payment and 2 todo payments, therefore 1 - 2 = -1. That's at least the logic in ourPaymentsSummary which I'm not sure makes any sense. It makes sense to rather be 1 out of 2 + 1, or "sent" out of "sent + todo".

Yes I agree, that's an excellent observation! Feel free to make the necessary changes to make that display 1 out of 3 Payments (or whatever makes sense for the particular scenario), and if you'd like me to do a deep dive into the code about this, let me know on Slack and I'll hop to it!

Yes I agree, that's an excellent observation! Feel free to make the necessary changes to make that display 1 out of 3 Payments (or whatever makes sense for the particular scenario), and if you'd like me to do a deep dive into the code about this, let me know on Slack and I'll hop to it!

Actually, what should happen here?

  • It says there are 2 TODO payments
  • You sent a partial payment
  • There are still 2 TODO payments
  • What should the "Payments sent" have for "out of"?

Should it be 1 out of 2, because there were 2 to begin with, and you only did a partial? Or should it include the partial payment that you sent, plus the current TODOs, and say 1 out of 3?

I think the way that it was designed is that part of the progress bar is supposed to turn a lighter shade of blue when there is a partial payment that exists, and the right-side of the "out of" should be the total number of users who you are expected to pay (including any late payments from last month!). So if there are 2 users you need to pay this month, then it should always show 2.

Also, it seems that the "user1 sends $71.43 to user2 (total)" test in Cypress is incorrect. It should end up having two TODOs, not one, because although user1 sent user2 some money, they still owe user2. Unless user4, who has some pledge, is supposed to take care of one of user2 or user3 fully, so that user1 only has one to take care of? I'm not sure what this test should be, and if it's correct or not.

Unless user4, who has some pledge, is supposed to take care of one of user2 or user3 fully, so that user1 only has one to take care of?

I believe there is never a scenario where, when using the proportional distribution algorithm, those who are pledging ever end up covering everything for a specific user (EDIT: unless there is only 1 person pledging). Rather, the proportional distribution algorithm is designed to ensure that everyone who is pledging will send some amount to all users who are classified as needing funds.

I think the way that it was designed is that part of the progress bar is supposed to turn a lighter shade of blue when there is a partial payment that exists

That's happening now, yeah.

and the right-side of the "out of" should be the total number of _users_ who you are expected to pay (including any late payments from last month!). So if there are 2 users you need to pay this month, then it should always show 2.

Oh. That was not what I assumed by the wording "Payments sent", which made me think that "1 out of 2" meant "1 payment sent out of 2 payments". Probably either the wording should be changed, or the purpose and semantics of this widget.

I agree, it might need a wording change... /cc @dotmacro

It's supposed to be like an "invoice", and so really there are 2 invoices that need to be paid, and if you partially pay it, there's still a single total invoice that needs payment.

Good point. If it's like an invoice, then only 2 payments were needed, even if there were 7 partials, so 7 out of 2 would make sense there in that case. Still, it just seems strange.

Good point as well, we can either change the wording, or change the number. /cc @dotmacro @mmbotelho your input (whenever you'd like) would be valuable on this!

Okay I figured some things out by turning this Cypress test into this unit test.

The Cypress test assumes that user1 should have a single todo payment of $178.57 to user3, but the unit tests shows that it actually results in two payments, split evenly between user2 and user3. It also has user4 splitting his money evenly between both user2 and user3.

I'm not sure what the app should be doing: have both pledgers give evenly to both needers, or have each pledger give to only one needer? I assume the first, since otherwise I don't think we can guarantee that their excess will be split evenly, in cases where the need is disproportionate, e.g. user2 needs $20 and user3 needs $100, but user1 has $1000 and user4 only has $20, then user4 is giving all he has (to either user) but user1 could afford to give more.

The Cypress test assumes that user1 should have a single todo payment of $178.57 to user3, but the unit tests shows that it actually results in two payments, split evenly between user2 and user3. It also has user4 splitting his money evenly between both user2 and user3.

I am not super familiar with these tests since I didn't write them, and am finding it difficult to find the section you're referring to. Could you assist me by pointing to the exact minimum number of line(s) where the assertion is failing/incorrect?

I'm not sure what the app should be doing: have both pledgers give evenly to both needers, or have each pledger give to only one needer? I assume the first

Given your description, your assumption would be correct. It is indeed correct when there are 2 pledging and 2 receiving, that β€” using the proportional distribution algorithm we're currently using β€” both pledgers will proportionally give to the receivers based on how much they pledged.

In that case I've updated the Cypress tests to match the unit tests. πŸ‘

Found a weird situation based on the first Cypress test case: If user1 and user4 are supposed to split money to user2, and user1 pays their full amount, presumably user1 is "done" paying, and user2 should no longer show up in user1's TODO list. However, the group income distribution algorithm only takes into account the current state, where user2 still needs money, and both user1 and user4 still have money to give. They should therefore still split the remaining need between their excesses. Therefore both still have user2 in their TODO list, even though user1 already paid their original amount; now they have a "new" amount to give (and user4 has less to give).

@sdegutis Yeah that sounds very similar to the scenario that my code was supposed to fix... I thought you said it had? Let's continue this convo on Slack to avoid sending too many emails.

Simplified example:

| User | Money |
|-|-|
| user1 | extra $100 |
| user2 | needs $20 |
| user3 | extra $100 |

Now both user1 and user3 have a todo-payment of $10 to user2.

If user1 sends $10 to user2, user2 still needs $10.

Now, both user1 and user3 have a todo-payment of $5 to user2.

I now understand Sandrina's comment better:

Why? It's the result ofΒ 85.71 - (25 - 14.29). The u2 now should only receive $14.29 instead of the needed $50. But u1 already sent $25, so the difference should be discounted from $85.71.

If user1 only owed $14.29 to user2, but gave $25, then even though user2 still has need, user1 owes them nothing anymore; and the excess that user1 gave user2 (25 - 14.29) should be deducted from what user1 was going to give to user3 (85.71), which ends up with $75.

@taoeffect Is this excess-redistribution behavior intended?

@taoeffect Is this excess-redistribution behavior intended?

Help me understand better, do you mean, "Should the app allow users to send more than they should to any given user?" or, "If they send more than they should, should it update what they owe to others?"

I personally think the app should not allow users to send more than requested to a given user. That should be an error in the UI and it should refuse to send payment. That would prevent confusion from happening in the first place, and would also be much fairer to the rest of the people who need $.

EDIT: misunderstood, see below

In Scenario 3's example, user1 owed user2 $14.29 and user3 $85.71 -- if they pay user2 $25, should they owe user3 less now (e.g. $75)? Or should they continue to owe the original $85.71 that they were going to pay?

if they pay user2 $25, should they owe user3 less now (e.g. $75)?

They shouldn't be able to register that they paid more than $14.29. That should be an error. So they should continue to owe $85.71

EDIT: misunderstood, see below

What do you mean? They shouldn't be able to record paying more than the distribution algorithm suggests that they pay (i.e. in "todo payments")? So that they can't enter more than $14.29 into the system?

Also, @taoeffect I figured out why your original code solution didn't work for Scenario 4.1, which involves recalculating after a member joins the group: the groupIncomeDistribution() function only receives a list of all current members. For that test to pass, we need to run the incomeDistribution() algorithm with only the members that existed when the group was first formed, but we don't have that information when the function is called. We could possibly try to derive it from each current member's join-group timestamp and compare it with the current month that we're calculating, to see if they should be included in the initial distribution algorithm.

If the only way to solve 4.1 is to keep track of when people joined, then we need a far more simpler way of keeping track of that than timestamps. For example, there could be an array stored for each month the current month of newly joined members during that month. Or an array of members who joined or left. Or a several arrays of all the members (although if there is a more compact way of representing that, we'd rather do it the more compact way, since some groups could have up to 100 members...). And this is only if that is the only way of solving that...

FYI, storing a full list of members is something I did try before, and didn't pursue, because I wanted to see if there was a way to do it without having to do that... Might be there is no way. I honestly do not know what the best solution is, and I'm entrusting this problem to you.

because I wanted to see if there was a way to do it without having to do that...

Why was that a goal? Were there problems with using too much memory or something?

Regarding Scenario 3, I misunderstood your questions up above about "overpayment". Copying from Slack:

In Sandrina's example, it's not how I read in your description of Scenario 3
The "overpayment" wasn't an overpayment, it happened because u3 updated their income details, and the algorithm decided that now they paid "more than they should have to u2", but it wasn't an overpayment in the sense I understood you describing it, so forget everything I said
In this case, Scenario 3 is something that needs to be handled
Since it wasn't originally an overpayment, and the only reason it appears to be now is because the distribution is generating different numbers since the income details were added for u3
She is right that the excess amount now need to be removed from what she owes to everyone else
In other words, you should never end up paying more than what you pledged originally

Regarding your other question above:

Why was that a goal? Were there problems with using too much memory or something?

Well, yes, we do want to avoid keeping too much in memory, because the app is already using up too much memory on mobile phones. So anything that is not strictly necessary to keep in memory we should avoid keeping in memory. However, if as you explore the solution-space you see no way to solve the problem than doing this, then by all means do it while being as efficient as you can.

I don't think Scenario 1 is possible to solve without user-income-change events.

For payment events and user-join events, we can derive that information based on the information available whenever the algorithm runs, based on member joined-dates and payment created-dates. But there's no way that I can see to recreate income-change events.

Consider all the data we have after all the events run:

should(groupIncomeDistributionNewLogic({
  haves: [
    { name: 'u1', have: 100 }
  ],
  needs: [
    { name: 'u2', need: 75 },
    { name: 'u3', need: 50 }
  ],
  events: [
  ]
})).eql([
  { amount: 60, from: 'u1', to: 'u2' },
  { amount: 40, from: 'u1', to: 'u3' },
])

You can see that u3 already has income details by the time the algorithm runs in the UI, and therefore the initial suggested payments include 60 and 40, splitting u1's income evenly between both u2 and u3.

Here's what it should have after we add a payment event:

should(groupIncomeDistributionNewLogic({
  haves: [
    { name: 'u1', have: 100 }
  ],
  needs: [
    { name: 'u2', need: 75 },
    { name: 'u3', need: 50 }
  ],
  events: [
    { type: 'payment', from: 'u1', to: 'u2', amount: 75 }
  ]
})).eql([
  // { amount: 60, from: 'u1', to: 'u2' },
  // paid via 75, and 75-60=15, and 40-15=25
  { amount: 25, from: 'u1', to: 'u3' }
])

We get to the right answer as in Scenario 1's original comment in this issue, that u1 should give u3 $25, but wet get to it the wrong way: it generated the 60/40 split, and after the fact, sees that 75 is more than 60, so it tries to redistribute this excess back into the other payment(s), and ends up with the right result.

However, this is the wrong reason to get to the right answer, and I think that in other circumstances, it will get the wrong answer too.

In particular, we redistribute the 25 between other payments made by u1, and in this case there is only one, so it works out. But the redistribution is necessarily going to be a simple average, splitting the payment evenly between all other payments.

In the event that there are other payments not also made by this user, they will then become unbalanced. If u1 was going to pay 30% of all the needs and u5 was going to pay 70%, then when u1's "excess" payment is redistributed back to all other members in need, the balance in the result will be shifted incorrectly, so that e.g. u1 is paying 40% and u5 is paying 60%, which ends up with the wrong final result.

So I don't think we can accurately encode this algorithm without at least being able to derive all events, i.e. income-changing, member-join/leave, and payment events, in this algorithm.

@sdegutis OK, I am reasonably convinced by your logic. You have green light to make necessary adjustments to generate events! πŸ‘

Very clearly explained. πŸ₯‡

We have two options for how to solve this:

  1. History: Store the whole event history in the DB, and "replay" them for the month that we're running the algorithm for, e.g. to display the donut chart in the UI.

  2. Mutation: Store only the initial and current state for each month, respectively representing passing adjusted as false or true in the currently live algorithm function, and then udpate the current state for the current month when each event actually happens.

Comparisons:

  • Algorithm complexity: The History solution has the disadvantage of having a slightly more complex algorithm, because it has to replay the history for the given month. But the Mutation solution has to be split up into multiple smaller functions, which aren't inherently grouped together, although we can ensure they are.

  • Memory usage: Due to this project's specific implementation of its decentralization goal, it stores all data in the browser for every client, although there is an issue (#691) to address this. The Mutation solution has the potential to use a ton of memory if a large group has a lot of activity for a given month. This may work against the browser memory limitations.

  • Work to get there: The History solution would probably be less invasive in the overall architecture, since it's merely adding events to the database. The Mutation solution would have to rip out a lot of the existing work for how the distribution data is stored, updated, and accessed, throughout the whole app.

For these reasons, it's hard for me to recommend either one of the solutions.

@sdegutis Excellent analysis of the pros & cons of each approach. Let's give the History approach a try.

Regarding #691 β€” This will likely be how most clients use the app. What this will do is it will not store the entire event log in IndexedDB, instead it will simply download the events, run through to create the state, and store only the latest state, discarding all of the events. It will store the latest Vuex state in IndexedDB instead (which includes the latest state for each subscribed contract), along with a pointer to the most recent event hash for each contract (something we already do, e.g. latestHash) so that it can continue to pick up from where it left off when it syncs them next time.

You are free to consider storing these events as totally separate events in the Vuex store. Please give careful consideration as far as to where you're storing them. For example, if, in order to calculate the latestContractState for the group contract, if it needs these events, then you will need to store them under a key within the group contract.

If that isn't needed, you can store them under a key in the top-level Vuex settings (which are local-only, specific to each client, and outside of any contract state).

If you do not understand any part of what I wrote above, then let's hop on a call, because it's super important to understand.

Oh there's actually one more caveat to the Mutation solution: we might have to have a cron job that sets the initial state at the beginning of each month. I'm not exactly sure what this solution would look like concretely, so it might not be necessary.

Hello everyone! I'm new and supposed to pick up where this issue left off.

If this is not the correct spot to be asking questions regarding this and #1000 / #1012 then please let me know the appropriate spot to do so.

I have created my own "groupIncomeDistributionAdjustFirstLogic" function for addressing this issue, and it seems that @sdegutis was quite right about not being about to handle scenario 3 and beyond without join / income-change events. Currently, the newly created "groupIncomeDistributionAdjustFirstLogic" gets to scenario 3, and then fails:

1) group income distribution logic
[scenario 3] redistributes excess of todo-payments back into other todo-payments:

AssertionError: expected Array [
Object { amount: 5.769230769230769, from: 'u1', to: 'u2' },
Object { amount: 69.23076923076924, from: 'u1', to: 'u3' }
] to equal Array [ Object { amount: 75, from: 'u1', to: 'u3' } ] (at length, A has 2 and B has 1)
+ expected - actual

[
    {
-    "amount": 5.769230769230769
+    "amount": 75
    "from": "u1"
-    "to": "u2"
-  }
-  {
-    "amount": 69.23076923076924
-    "from": "u1"
    "to": "u3"
    }
]

I will post updates here.

So just documenting where we are. Here's the current approach we're experimenting with called "adjust first":

export function groupIncomeDistributionAdjustFirstLogic ({ getters, monthstamp, adjusted }) {
  const groupProfiles = getters.groupProfiles
  const mincomeAmount = getters.groupSettings.mincomeAmount
  const monthlyPayments = getters.groupMonthlyPayments
  const allPayments = getters.currentGroupState.payments
  const thisMonthPayments = monthlyPayments[monthstamp]
  const paymentsFrom = thisMonthPayments && thisMonthPayments.paymentsFrom

  const haves = []
  const needs = []
  // calculate haves and needs from pledges and incomes:
  for (const username in groupProfiles) {
    const profile = groupProfiles[username]
    const incomeDetailsType = profile && profile.incomeDetailsType
    if (incomeDetailsType === 'incomeAmount') {
      needs.push({ name: username, need: mincomeAmount - profile.incomeAmount })
    } else if (incomeDetailsType === 'pledgeAmount') {
      haves.push({ name: username, have: profile.pledgeAmount })
    }
  }
  /// Adjust haves/needs if `adjusted = true`
  if (adjusted) {
    const alreadySent = {}
    const alreadyReceived = {}
    if (paymentsFrom) {
      for (const fromUser in paymentsFrom) {
        let totalSent = 0
        for (const toUser in paymentsFrom[fromUser]) {
          let totalReceved = 0
          if (!alreadyReceived[toUser]) alreadyReceived[toUser] = 0
          for (const paymentHash of paymentsFrom[fromUser][toUser]) {
            totalReceved += allPayments[paymentHash].amount
          }
          totalSent += totalReceved
          alreadyReceived[toUser] += totalReceved
        }
        alreadySent[fromUser] = totalSent
      }
      for (const have of haves) {
        have.have -= alreadySent[have.name]
      }
      for (const need of needs) {
        need.need -= alreadyReceived[need.name]
      }
    }
  }
  /// pass the haves and needs to distributeFromHavesToNeeds
  return distibuteFromHavesToNeeds({ haves, needs })
}

Thanks for cleaning up the code @taoeffect!

I have taken the liberty of implementing this function (while fixing a few bugs) and this is what it became:

export function groupIncomeDistributionAdjustFirstLogic ({ getters, monthstamp, adjusted }) {
  const groupProfiles = getters.groupProfiles
  const mincomeAmount = getters.groupSettings.mincomeAmount
  const allPayments = getters.currentGroupState.payments
  const thisMonthPayments = getters.monthlyPayments[monthstamp]
  const paymentsFrom = thisMonthPayments && thisMonthPayments.paymentsFrom

  const haves = []
  const needs = []
  // calculate haves and needs from pledges and incomes:
  for (const username in groupProfiles) {
    const profile = groupProfiles[username]
    const incomeDetailsType = profile && profile.incomeDetailsType
    if (incomeDetailsType === 'incomeAmount') {
      needs.push({ name: username, need: mincomeAmount - profile.incomeAmount })
    } else if (incomeDetailsType === 'pledgeAmount') {
      haves.push({ name: username, have: profile.pledgeAmount })
    }
  }
  /// Adjust haves/needs if `adjusted = true`
  if (adjusted) {
    const alreadySent = {}
    const alreadyReceived = {}
    if (paymentsFrom) {
      for (const fromUser in paymentsFrom) {
        let totalSent = 0
        for (const toUser in paymentsFrom[fromUser]) {
          let totalReceved = 0
          if (!alreadyReceived[toUser]) alreadyReceived[toUser] = 0
          for (const paymentHash of paymentsFrom[fromUser][toUser]) {
            totalReceved += allPayments[paymentHash].data.amount
          }
          totalSent += totalReceved
          alreadyReceived[toUser] += totalReceved
        }
        alreadySent[fromUser] = totalSent
      }
      for (const have of haves) {
        have.have -= alreadySent[have.name] ? alreadySent[have.name] : 0
      }
      for (const need of needs) {
        need.need -= alreadyReceived[need.name] ? alreadyReceived[need.name] : 0
      }
    }
  }
  /// pass the haves and needs to distributeFromHavesToNeeds
  const dist = distibuteFromHavesToNeeds({ haves, needs }).filter((payment)=>{
    return payment.amount > 0
  })
  return dist
}

I have also scraped all the JSON of the parameters for each of the scenarios. To do this, I inserted this code into the master branch:

  console.debug(
    `=============BEGIN PARAMETERS=============
    {
      getters:
      {
        groupProfiles: ` + JSON.stringify(getters.groupProfiles) + `,
        groupSettings: ` + JSON.stringify(getters.groupSettings) + `,
        monthlyPayments: ` + JSON.stringify(getters.groupMonthlyPayments) + `,
        currentGroupState: ` + JSON.stringify(getters.currentGroupState) + `,
      },
      monthstamp: "` + monthstamp + `",
      adjusted: ` + adjusted + `
    }
    =============END PARAMETERS=============`);

Attached to this message is all the scraped scenario JSON in a file.

payment-scenarios.js.txt

All of this ends up failing on [Scenario 3]:

AssertionError: expected Array [
  Object { amount: 5.769230769230769, from: 'u1', to: 'u2' },
  Object { amount: 69.23076923076924, from: 'u1', to: 'u3' }
] to equal Array [ Object { amount: 75, from: 'u1', to: 'u3' } ]

Additionally, if I skip [Scenario 3], it passes up until the following scenario:

  1) group income distribution *adjust first* logic
       splits money evenly between two pledgers and two needers:
[
  Object { amount: 71.42857142857143, from: 'u1', to: 'u2' },
  Object { amount: 8.163265306122447, from: 'u4', to: 'u2' },
  Object { amount: 178.57142857142858, from: 'u1', to: 'u3' },
  Object { amount: 20.40816326530612, from: 'u4', to: 'u3' }
] to equal Array [
  Object { amount: 71.42857143, from: 'u1', to: 'u2' },
  Object { amount: 178.57142857, from: 'u1', to: 'u3' },
  Object { amount: 28.57142857, from: 'u4', to: 'u2' },
  Object { amount: 71.42857143, from: 'u4', to: 'u3' }
]

I am going to spend some time double checking whether the last scenario was set up correctly during the JSON scraping.

Any thoughts or suggestions would be greatly appreciated.

Here is the JavaScript code I used to strip the JSON objects down to what you see in my last commit:

  let newJSON = {}
  newJSON.groupProfiles = {}
  newJSON.currentGroupState = {}
  newJSON.monthlyPayments = {}
  for (const username in getters.groupProfiles) {
    newJSON.groupProfiles[username] = {};
    newJSON.groupProfiles[username].incomeDetailsType = getters.groupProfiles[username].incomeDetailsType;
    newJSON.groupProfiles[username].incomeAmount = getters.groupProfiles[username].incomeAmount;
    newJSON.groupProfiles[username].pledgeAmount = getters.groupProfiles[username].pledgeAmount;
  }

  newJSON.monthlyPayments = {}
  newJSON.monthlyPayments[monthstamp] = {}
  newJSON.monthlyPayments[monthstamp].paymentsFrom = {}

  newJSON.groupSettings = {}
  newJSON.groupSettings.mincomeAmount = getters.groupSettings.mincomeAmount

  newJSON.currentGroupState = {}
  newJSON.currentGroupState.payments = {}

  let payment = 0
  for (const fromUser in getters.monthlyPayments[monthstamp].paymentsFrom) {
    newJSON.monthlyPayments[monthstamp].paymentsFrom[fromUser] = {}
    for (const toUser in getters.monthlyPayments[monthstamp].paymentsFrom[fromUser]) {
      newJSON.monthlyPayments[monthstamp].paymentsFrom[fromUser][toUser] = []
      for (const paymentHash of getters.monthlyPayments[monthstamp].paymentsFrom[fromUser][toUser]) {
        payment++
        const theHash = "paymentHsh"+payment//paymentHash)
        newJSON.monthlyPayments[monthstamp].paymentsFrom[fromUser][toUser].push(theHash)
        newJSON.currentGroupState.payments[theHash] = {}
        newJSON.currentGroupState.payments[theHash].data = {}
        newJSON.currentGroupState.payments[theHash].data.amount = getters.currentGroupState.payments[paymentHash].data.amount
      }
    }
  }

  console.debug(JSON.stringify(newJSON))

Here's another mocha test scenario we should handle that goes between two months, an interesting Late payment / overpayment possibility:

Setup:

  • Every month algorithm says A sends B $20
  • Last month A only sent B $10
  • So this month A has to send B $10 (Late Payment) + $20 for this month
  • So, A sends B $20
  • BUT, then B adjusts their income details (or maybe someone else does), and A's TODO list for B has now changed from $20 / month, to $10 / month
  • So, since A sent B $20 this month, then the late payment of $10 has already been knocked out, plus A has paid for this month too

We need this as a mocha scenario too.

And we should also come up with a scenario where someone changes their income details from having to needing (& vice-versa) in the middle of the month.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mmbotelho picture mmbotelho  Β·  7Comments

mmbotelho picture mmbotelho  Β·  6Comments

sandrina-p picture sandrina-p  Β·  5Comments

taoeffect picture taoeffect  Β·  5Comments

sandrina-p picture sandrina-p  Β·  8Comments