From 6aef766da61f24de3436d59d3c1f10cbf1ee74ce Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Mon, 6 May 2024 22:27:10 -0500 Subject: [PATCH 01/11] Add stories collection --- astro/src/content/config.ts | 11 ++++ .../content/stories/patronage/1-primer.mdx | 57 +++++++++++++++++++ astro/src/pages/index.astro | 12 ++++ astro/src/pages/stories/[...slug].astro | 18 ++++++ 4 files changed, 98 insertions(+) create mode 100644 astro/src/content/stories/patronage/1-primer.mdx create mode 100644 astro/src/pages/stories/[...slug].astro diff --git a/astro/src/content/config.ts b/astro/src/content/config.ts index dc2bbd8..9a022b2 100644 --- a/astro/src/content/config.ts +++ b/astro/src/content/config.ts @@ -1,5 +1,6 @@ // Import utilities from `astro:content` import { z, defineCollection } from "astro:content"; + // Define a `type` and `schema` for each collection const documentsCollection = defineCollection({ type: 'content', @@ -8,7 +9,17 @@ const documentsCollection = defineCollection({ description: z.string(), }) }); + +const storiesCollection = defineCollection({ + type: 'content', + schema: z.object({ + title: z.string(), + description: z.string(), + }) +}); + // Export a single `collections` object to register your collection(s) export const collections = { documents: documentsCollection, + stories: storiesCollection, }; diff --git a/astro/src/content/stories/patronage/1-primer.mdx b/astro/src/content/stories/patronage/1-primer.mdx new file mode 100644 index 0000000..3edd1b2 --- /dev/null +++ b/astro/src/content/stories/patronage/1-primer.mdx @@ -0,0 +1,57 @@ +--- +title: Patronage Primer +description: A primer on how Patronage is set up for Dark Patterns Digital +--- + +# Patronage + +The concept of Patronage is common to cooperatives, but each one does it a bit +differently. Patronage is essentially the measure by which surplus generated by +a co-op is distributed back to its members. Patronage can be calculated many +ways: + +- Number of dollars spent with a retailer cooperative, like REI +- Amount of products produced for a producer cooperative, like Ocean Spray +- Amount of services purchased, like in state-recognized healthcare co-ops +- Number of hours worked, such as in co-ops like ours + +Patronage can be granted by a co-op for any number of activities that the co-op +wishes to encourage, but primarily it is granted for efforts contributed to the +coop. + +## Patronage for Dark Patterns + +As a technical consulting co-op, our patronage allocations will be more +complicated than "hours worked". As of this writing, the relevant definitions +include: + +- "Tenure" shall be accumulated on a per-week basis, for all Members and + Prospective Members within their candidacy periods, Sunday through Saturday, + as the greatest of the following: + + * 1 if at least 1 hour was worked for the Cooperative, or if an employee + of the Cooperative. + * 2 if at least 8 hours were worked for the Cooperative. + * 3 if at least 16 hours were worked for the Cooperative. + * 4 if at least 24 hours were worked for the Cooperative. + * 5 if at least 32 hours were worked for the Cooperative. + +- "Patronage" shall be calculated per Member within a fiscal year as the sum of + the following: + + * Tenure accumulated within the current fiscal year; + * A number as determined via Patronage Schedules; plus + * The lesser of (a) the Member's total Tenure at the end of the fiscal year + or (b) twice the Patronage earned in the above. + +## Tenure + +TODO + +## Patronage + +TODO + +## Example + +TODO diff --git a/astro/src/pages/index.astro b/astro/src/pages/index.astro index 5b45359..c407846 100644 --- a/astro/src/pages/index.astro +++ b/astro/src/pages/index.astro @@ -5,6 +5,7 @@ import mdStyles from '../components/sets/documents/md-styles.module.css' import { organizationName, orgAbbrev } from '../constants' const documents = await getCollection('documents'); +const stories = await getCollection('stories'); --- For now, open each page and use "Print to PDF" within your browser.

+

Explanations

+ +

Concepts, such as Patronage, are hard to understand from just the definitions themselves. The following pages help to explain how it works in practice.

+ + +
diff --git a/astro/src/pages/stories/[...slug].astro b/astro/src/pages/stories/[...slug].astro new file mode 100644 index 0000000..12f12ce --- /dev/null +++ b/astro/src/pages/stories/[...slug].astro @@ -0,0 +1,18 @@ +--- +import { getCollection } from 'astro:content'; +import Layout from '../../layouts/Layout.astro'; +import mdStyles from '../../components/sets/documents/md-styles.module.css' + +export async function getStaticPaths() { + const blogEntries = await getCollection('stories'); + return blogEntries.map(entry => ({ + params: { slug: entry.slug }, props: { entry }, + })); +} + +const { entry } = Astro.props; +const { Content } = await entry.render(); +--- + + + From 910b293639273563c5c691f3826aa6d84754dbe9 Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Tue, 7 May 2024 07:09:12 -0500 Subject: [PATCH 02/11] Finish writing primer on patronage --- astro/src/content/config.ts | 1 + astro/src/content/documents/bylaws.mdx | 4 +- .../content/stories/patronage/1-primer.mdx | 63 +++++++++++++++++-- astro/src/pages/index.astro | 1 + 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/astro/src/content/config.ts b/astro/src/content/config.ts index 9a022b2..981fed3 100644 --- a/astro/src/content/config.ts +++ b/astro/src/content/config.ts @@ -15,6 +15,7 @@ const storiesCollection = defineCollection({ schema: z.object({ title: z.string(), description: z.string(), + order: z.number(), }) }); diff --git a/astro/src/content/documents/bylaws.mdx b/astro/src/content/documents/bylaws.mdx index d3e840a..c16b37b 100644 --- a/astro/src/content/documents/bylaws.mdx +++ b/astro/src/content/documents/bylaws.mdx @@ -116,7 +116,7 @@ https://drive.google.com/file/d/0B7aFfmk4wQfbWVRPQzFkU1ZHLVE/view?resourcekey=0- responsible for any tax liability incurred as a result of Patronage with the Cooperative, as defined in . Each Member shall indemnify and forever hold harmless the Cooperative from any claims of any kind arising out - of their patronage or their Membership of the Cooperative. + of their Patronage or their Membership of the Cooperative. 9. Record of Members. A record of the Members and their full names, addresses, and, if required for tax reporting purposes, social security or tax @@ -769,7 +769,7 @@ https://drive.google.com/file/d/0B7aFfmk4wQfbWVRPQzFkU1ZHLVE/view?resourcekey=0- amounts of Patronage may be allocated to Members. 2. The Patronage Schedule shall have an effective date no earlier than 120 days after approval by the Board or a committee appointed by the board to - handle patronage. + handle Patronage. 3. Within 30 days of the approval of a new Patronage Schedule, the President or Secretary shall cause notice to be given to the Members including the new Patronage Schedule. diff --git a/astro/src/content/stories/patronage/1-primer.mdx b/astro/src/content/stories/patronage/1-primer.mdx index 3edd1b2..10b7571 100644 --- a/astro/src/content/stories/patronage/1-primer.mdx +++ b/astro/src/content/stories/patronage/1-primer.mdx @@ -1,6 +1,7 @@ --- title: Patronage Primer description: A primer on how Patronage is set up for Dark Patterns Digital +order: 0 --- # Patronage @@ -25,6 +26,8 @@ As a technical consulting co-op, our patronage allocations will be more complicated than "hours worked". As of this writing, the relevant definitions include: +
+ - "Tenure" shall be accumulated on a per-week basis, for all Members and Prospective Members within their candidacy periods, Sunday through Saturday, as the greatest of the following: @@ -44,14 +47,62 @@ include: * The lesser of (a) the Member's total Tenure at the end of the fiscal year or (b) twice the Patronage earned in the above. -## Tenure +- While no Patronage Schedule is in effect, the amount of determined via + Patronage Schedules shall be calculated as 1 for every $5,000 paid to the + Cooperative via executed contracts referred by the Member. -TODO +
-## Patronage +## Tenure -TODO +Tenure is designed to give a measure of how long someone has worked for the +cooperative while allowing a part-time worker to accumulate Tenure at a slower +pace than full-time workers. + +- The work-week is defined for Tenure as Sunday through Saturday so that a + member who works overtime during a weekend can more easily reach maximum + Tenure during both the preceding and the following week. (This will also be + helpful for workers who are moonlighting for the coop, as working both + Saturday and Sunday in the same weekend will count as two separate weeks.) +- Any amount of work for the Cooperative earns 1 point of Tenure, even if it is + only 1 hour. + - Once we start employing people regularly, 1 point of Tenure will be + accumulated even during a week of PTO. + - Board meetings and correct accounting are very important for a successful + business; Tenure should be accumulated for that time, too, even when not + employed. +- Tenure reaches its maximum with 32 hours per week. This should encourage us to + not continually work 40 hour weeks, but encourage more frequent long weekends. -## Example +## Patronage -TODO +Patronage is the number by which surplus (revenue generated by Members in excess +of expenses) is distributed to the Members. Patronage is frequently used by +coops as a way to encourage particular behaviors. As such, Patronage is defined +with a few key points: + +1. Encourage the same concepts as Tenure; as workers, our time is the most + valuable resource we bring to the coop. +2. Scale up over time. A new Member to the coop will not receive as much + Patronage as a Member who has accumulated Tenure in previous years; this is + to reflect onboarding time and to encourage a constant stream of growth. (See + the examples for how this encourages growth.) +3. Show appreciation for contracts brought to the coop. Especially in our + initial phase, new contracts will likely earn a commission; instead, + additional Patronage is earned. + +We have the ability to redefine the Patronage Schedule without an amendment to +the Bylaws so that we may add other non-labor incentives later. Some examples +that have already been discussed: + +- Volunteer work +- Tutoring for college students in the fields in which we work +- Participating in local meet-up groups + +### A note about Surplus + +As a coop, just like in an LLC, tax reporting is the responsibility of the +individual Members. Surplus is distinct from Profits because federal tax law +says that only “patronage-sourced” income may be distributed as tax-deductible +patronage refunds/patronage dividends. As a result, money paid as patronage +dividends or advances on patronage dividends have a special tax class. diff --git a/astro/src/pages/index.astro b/astro/src/pages/index.astro index c407846..2c1bc13 100644 --- a/astro/src/pages/index.astro +++ b/astro/src/pages/index.astro @@ -6,6 +6,7 @@ import { organizationName, orgAbbrev } from '../constants' const documents = await getCollection('documents'); const stories = await getCollection('stories'); +stories.sort((a, b) => b.data.order - a.data.order); --- Date: Tue, 7 May 2024 07:37:25 -0500 Subject: [PATCH 03/11] Add links between stories --- .../sets/documents/legal-styles.module.css | 34 +++++++++++++++++-- .../sets/documents/md-styles.module.css | 12 ++++--- astro/src/pages/stories/[...slug].astro | 34 ++++++++++++++++--- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/astro/src/components/sets/documents/legal-styles.module.css b/astro/src/components/sets/documents/legal-styles.module.css index 5432da8..1b87ba6 100644 --- a/astro/src/components/sets/documents/legal-styles.module.css +++ b/astro/src/components/sets/documents/legal-styles.module.css @@ -1,7 +1,35 @@ -@import url(./md-styles.module.css); -.container { - composes: container from './md-styles.module.css'; +.container :global h1 { + @apply text-lg font-bold text-center; + font-variant: small-caps; +} + +.container :global h2 { + @apply font-bold text-center; + counter-increment: article; + font-variant: small-caps; +} + +.container :global a { + @apply text-blue-700 dark:text-blue-400 underline; +} +.container :global p { + @apply my-4; +} +.container :global ol { + @apply my-4; + --indentSize: 4rem; + counter-reset: item; + margin-left: var(--indentSize); +} +.container :global li { + @apply my-4; +} +.container :global ul { + @apply my-4 ml-6 list-disc; +} +.container :global blockquote { + @apply my-4 px-4 border-l-emerald-500 border-l-4; } .container :global h2:before { diff --git a/astro/src/components/sets/documents/md-styles.module.css b/astro/src/components/sets/documents/md-styles.module.css index f3adc3b..eca9b9b 100644 --- a/astro/src/components/sets/documents/md-styles.module.css +++ b/astro/src/components/sets/documents/md-styles.module.css @@ -1,12 +1,16 @@ .container :global h1 { - @apply text-lg font-bold text-center; + @apply text-2xl font-bold; font-variant: small-caps; } .container :global h2 { - @apply font-bold text-center; - counter-increment: article; + @apply text-xl font-bold; + font-variant: small-caps; +} + +.container :global h3 { + @apply text-lg font-bold; font-variant: small-caps; } @@ -29,5 +33,5 @@ @apply my-4 ml-6 list-disc; } .container :global blockquote { - @apply my-4 px-4 border-l-emerald-500 border-l-4; + @apply my-4 px-4 border-l-slate-500 border-l-4; } diff --git a/astro/src/pages/stories/[...slug].astro b/astro/src/pages/stories/[...slug].astro index 12f12ce..5f6eea8 100644 --- a/astro/src/pages/stories/[...slug].astro +++ b/astro/src/pages/stories/[...slug].astro @@ -5,14 +5,38 @@ import mdStyles from '../../components/sets/documents/md-styles.module.css' export async function getStaticPaths() { const blogEntries = await getCollection('stories'); - return blogEntries.map(entry => ({ - params: { slug: entry.slug }, props: { entry }, - })); + return blogEntries.map((entry, index, allEntries) => { + const prev = allEntries[index - 1]; + const next = allEntries[index + 1]; + return { + params: { slug: entry.slug }, + props: { + entry, + prev: prev ? { + title: prev.data.title, + slug: prev.slug, + } : null, + next: next ? { + title: next.data.title, + slug: next.slug, + } : null, + }, + }; + }); } -const { entry } = Astro.props; +const { entry, prev, next } = Astro.props; const { Content } = await entry.render(); --- - + +

Keep Reading

+
+
+ {prev ? {prev.title} : null} +
+
+ {next ? {next.title} : null} +
+
From a621d83cc2d8d8806193917eebc49fe31b49170b Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Tue, 7 May 2024 07:58:52 -0500 Subject: [PATCH 04/11] Correct order --- astro/src/pages/index.astro | 2 +- astro/src/pages/stories/[...slug].astro | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/astro/src/pages/index.astro b/astro/src/pages/index.astro index 2c1bc13..a81cd6d 100644 --- a/astro/src/pages/index.astro +++ b/astro/src/pages/index.astro @@ -6,7 +6,7 @@ import { organizationName, orgAbbrev } from '../constants' const documents = await getCollection('documents'); const stories = await getCollection('stories'); -stories.sort((a, b) => b.data.order - a.data.order); +stories.sort((a, b) => a.data.order - b.data.order); --- a.data.order - b.data.order); return blogEntries.map((entry, index, allEntries) => { const prev = allEntries[index - 1]; const next = allEntries[index + 1]; From 7beac82e0d66c4846e3a0881b5f0b331e8694dfc Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Tue, 7 May 2024 07:59:01 -0500 Subject: [PATCH 05/11] Initial patronage calculation utilities --- astro/src/business-logic/patronage.ts | 32 +++++++++++++++++++ .../stories/patronage/2-basic-example.mdx | 14 ++++++++ 2 files changed, 46 insertions(+) create mode 100644 astro/src/business-logic/patronage.ts create mode 100644 astro/src/content/stories/patronage/2-basic-example.mdx diff --git a/astro/src/business-logic/patronage.ts b/astro/src/business-logic/patronage.ts new file mode 100644 index 0000000..8069422 --- /dev/null +++ b/astro/src/business-logic/patronage.ts @@ -0,0 +1,32 @@ +import { parseISO } from 'date-fns'; + +interface PatronageEvents { + addMember: { name: string; tenureRatePerWeek: number }; + tenureRate: { name: string; tenureRatePerWeek: number }; + patronage: { name: string; additional: number; comment: string }; +} + +type PatronageEvent = + & { date: Date } + & { + [P in keyof PatronageEvents]: { type: P } & PatronageEvents[P] + }[keyof PatronageEvents]; + + +export class PatronageRecord { + private events: PatronageEvent[] = []; + + add(date: Date | string, type: P, data: PatronageEvents[P]): this { + if (typeof date === 'string') + date = parseISO(date); + const after = this.events.findIndex(e => e.date > date); + const newEvent = { date, type, ...data } as PatronageEvent; + if (after === -1) + this.events.push(newEvent); + else + this.events.splice(after, 0, newEvent); + return this; + } + + get allEvents() { return [...this.events]; } +} \ No newline at end of file diff --git a/astro/src/content/stories/patronage/2-basic-example.mdx b/astro/src/content/stories/patronage/2-basic-example.mdx new file mode 100644 index 0000000..7abaa97 --- /dev/null +++ b/astro/src/content/stories/patronage/2-basic-example.mdx @@ -0,0 +1,14 @@ +--- +title: Patronage Basic Example +description: A simple example on how Patronage is intended to work +order: 10 +--- + +import { PatronageRecord } from '../../../business-logic/patronage'; + +export const patronageExample = new PatronageRecord() + .add('2024-05-01', 'addMember', { name: 'Alf', tenureRatePerWeek: 5 }) + .add('2024-08-01', 'addMember', { name: 'Betty', tenureRatePerWeek: 5 }) + .add('2025-01-01', 'addMember', { name: 'Caitlin', tenureRatePerWeek: 5 }) + +
{JSON.stringify(patronageExample.events, undefined, '  ')}
\ No newline at end of file From b77c19856994ba55e69ee360ce1ab24b51ab188f Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Wed, 8 May 2024 07:08:40 -0500 Subject: [PATCH 06/11] Display point-in-time graphically --- astro/src/business-logic/patronage.ts | 99 +++++++++++++++++-- .../components/patronage/point-in-time.tsx | 81 +++++++++++++++ .../sets/documents/md-styles.module.css | 5 +- .../stories/patronage/2-basic-example.mdx | 49 ++++++++- 4 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 astro/src/components/patronage/point-in-time.tsx diff --git a/astro/src/business-logic/patronage.ts b/astro/src/business-logic/patronage.ts index 8069422..40058fc 100644 --- a/astro/src/business-logic/patronage.ts +++ b/astro/src/business-logic/patronage.ts @@ -1,24 +1,52 @@ -import { parseISO } from 'date-fns'; +import { date } from 'astro/zod'; +import { parseISO, addDays } from 'date-fns'; -interface PatronageEvents { +export interface PatronageEvents { addMember: { name: string; tenureRatePerWeek: number }; tenureRate: { name: string; tenureRatePerWeek: number }; patronage: { name: string; additional: number; comment: string }; } -type PatronageEvent = +export type PatronageEvent = & { date: Date } & { [P in keyof PatronageEvents]: { type: P } & PatronageEvents[P] - }[keyof PatronageEvents]; + }[T]; +export type PatronageEntry = { + memberDate: Date; + weeksYTD: number; + tenurePerWeek: number; + tenure: number; + tenureYTD: number; + patronageScheduleYTD: number; + patronageYTD: number; +}; + +function normalizeDate(date: Date | string): Date { + return typeof date === 'string' ? parseISO(date) : date; +} + +function newPatronageEntry(memberDate: Date): PatronageEntry { + return { + memberDate, + weeksYTD: 0, + tenurePerWeek: 0, + tenure: 0, + tenureYTD: 0, + patronageScheduleYTD: 0, + patronageYTD: 0, + }; +} export class PatronageRecord { - private events: PatronageEvent[] = []; + private events: PatronageEvent[]; + constructor(events?: PatronageEvent[]) { + this.events = events ? [...events] : []; + } add(date: Date | string, type: P, data: PatronageEvents[P]): this { - if (typeof date === 'string') - date = parseISO(date); + date = normalizeDate(date); const after = this.events.findIndex(e => e.date > date); const newEvent = { date, type, ...data } as PatronageEvent; if (after === -1) @@ -28,5 +56,62 @@ export class PatronageRecord { return this; } + getDetailsAt(date: Date | string): Record { + date = normalizeDate(date); + const results: Record = {}; + if (!this.events[0]) return results; + let nextEvent: PatronageEvent | undefined; + for (let current = this.events[0].date, nextEventIndex = 0; current <= date; current = addDays(current, 1)) { + // reset from end of year + if (current.getMonth() === 0 && current.getDate() === 1) + for (const entry of Object.values(results)) { + entry.patronageScheduleYTD = 0; + entry.patronageYTD = 0; + entry.tenureYTD = 0; + } + + // process events + while ((nextEvent = this.events[nextEventIndex]) && nextEvent.date <= current) { + nextEventIndex++; + switch (nextEvent.type) { + case 'addMember': + const newMember = newPatronageEntry(current); + newMember.tenurePerWeek = nextEvent.tenureRatePerWeek; + results[nextEvent.name] = newMember; + break; + case 'patronage': + results[nextEvent.name]!.patronageScheduleYTD += nextEvent.additional; + break; + case 'tenureRate': + results[nextEvent.name]!.tenurePerWeek = nextEvent.tenureRatePerWeek; + break; + } + } + + // process tenure + if (current.getDay() === 5 /* Saturday */) { + // TODO: it's unclear in the bylaws when Tenure is allocated; + // what happens for partial weeks at beginning/end of the year? + // This code assumes that all tenure is allocated on Saturday at + // the end of the week. + for (const entry of Object.values(results)) { + entry.weeksYTD += 1; + entry.tenure += entry.tenurePerWeek; + entry.tenureYTD += entry.tenurePerWeek + } + } + } + + for (const entry of Object.values(results)) { + const base = entry.tenureYTD + entry.patronageScheduleYTD; + entry.patronageYTD = base + Math.min(entry.tenure, base * 2); + } + return results; + } + get allEvents() { return [...this.events]; } + + clone(): PatronageRecord { + return new PatronageRecord(this.events); + } } \ No newline at end of file diff --git a/astro/src/components/patronage/point-in-time.tsx b/astro/src/components/patronage/point-in-time.tsx new file mode 100644 index 0000000..824ee43 --- /dev/null +++ b/astro/src/components/patronage/point-in-time.tsx @@ -0,0 +1,81 @@ +import { compareAsc, format } from 'date-fns'; +import type { PatronageEntry } from "../../business-logic/patronage"; +const { amber, blue, cyan, emerald, orange, purple, rose } = (await import('tailwindcss/colors')).default; + +const colorIndices = [ + 500, + 900, + 100, + 700, + 300, +] as const; +type ColorIndices = typeof colorIndices[number]; + +const colorMaps: { [P in ColorIndices]: string }[] = [ + amber, + emerald, + blue, + orange, + cyan, + purple, + rose +]; + +const colors = (Array(colorIndices.length * colorMaps.length).fill(0).map((_, index) => { + const colorRange = colorMaps[index % colorMaps.length]!; + return colorRange[colorIndices[index % colorIndices.length]!]; +})) + +export function PointInTime({ data, membersOrder }: { data: Record, membersOrder?: string[] }) { + const allEntries = Object.entries(data); + const order = membersOrder ?? allEntries.sort((a, b) => compareAsc(a[1].memberDate, b[1].memberDate)).map(([name]) => name); + return ( + <> +
+ {order.map((name, index) => +
+ )} +
+
+ +
+ {order.map((name, index) => +
+ + {' '}{name} +
+ )} +
+
+ + + + + + + + + + + + + {order.map((name, index) => + { + const current = data[name]!; + return + + + + + + + ; + } + )} + +
NameMember DateWeeks YTDTenure (YTD/total)Patronage Schedule (YTD)Patronage (YTD)
+ {' '}{name}{format(current.memberDate, 'MMM d, yyyy')}{current.weeksYTD.toFixed(0)}{current.tenureYTD.toFixed(0)} / {current.tenure.toFixed(0)}{current.patronageScheduleYTD.toFixed(0)}{current.patronageYTD.toFixed(0)}
+
+ + ) +} \ No newline at end of file diff --git a/astro/src/components/sets/documents/md-styles.module.css b/astro/src/components/sets/documents/md-styles.module.css index eca9b9b..3a73330 100644 --- a/astro/src/components/sets/documents/md-styles.module.css +++ b/astro/src/components/sets/documents/md-styles.module.css @@ -21,10 +21,7 @@ @apply my-4; } .container :global ol { - @apply my-4; - --indentSize: 4rem; - counter-reset: item; - margin-left: var(--indentSize); + @apply my-4 ml-6 list-decimal; } .container :global li { @apply my-4; diff --git a/astro/src/content/stories/patronage/2-basic-example.mdx b/astro/src/content/stories/patronage/2-basic-example.mdx index 7abaa97..9ba53b9 100644 --- a/astro/src/content/stories/patronage/2-basic-example.mdx +++ b/astro/src/content/stories/patronage/2-basic-example.mdx @@ -5,10 +5,51 @@ order: 10 --- import { PatronageRecord } from '../../../business-logic/patronage'; +import { PointInTime } from '../../../components/patronage/point-in-time'; export const patronageExample = new PatronageRecord() - .add('2024-05-01', 'addMember', { name: 'Alf', tenureRatePerWeek: 5 }) - .add('2024-08-01', 'addMember', { name: 'Betty', tenureRatePerWeek: 5 }) - .add('2025-01-01', 'addMember', { name: 'Caitlin', tenureRatePerWeek: 5 }) + .add('2024-04-29', 'addMember', { name: 'Alfred', tenureRatePerWeek: 5 }) + .add('2024-07-29', 'addMember', { name: 'Betty', tenureRatePerWeek: 5 }) + .add('2024-12-30', 'addMember', { name: 'Caitlin', tenureRatePerWeek: 5 }) -
{JSON.stringify(patronageExample.events, undefined, '  ')}
\ No newline at end of file +As a simple example, let's consider the following events: + +1. On Monday, April 29, 2024, Alfred starts the coop and he is all in, working a full-time + 40-hours every week. Every week, Alfred earns 5 tenure points. +2. On Monday, July 29, 2024, Betty joins Alfred, working a full-time 40 hours every + week. Starting from then, both Alfred and Betty are earning 5 tenure points + every week. +3. On Monday, December 30, 2024, Caitlin joins them both, also managing a full-time 40 + hours every week. Starting with the end of that week, all three of them are + earning 5 tenure points every week. + +Before Betty joined, it didn't matter how much Alfred earned; any surplus +distributed would be allocated entirely to him. + + + +However, the week after Betty starts, Betty begins to accumulate patronage: + + + +By the end of Q3, Betty has worked for 2 full months and Alfred has worked for +5, and the allocation of patronage reflects this split: + + + +At the end of the year, Caitlin joins, but does not work a full week before the +end of year calculations are made. + + + +Assuming no further team members are added, by the end of the following year, +even though all three worked full-time for the entire year, Alfred has slightly +more patronage due to having started earning tenure earlier. + + + +However, by the end of the year after that, all the tenure has reached its +maximum contribution to patronage; as a result, the patronage shares have +stabilized with all three members earning an equal share. + + From 0f43c702624a4d08af169e947ad81b5a9ff58e4d Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Wed, 8 May 2024 07:08:49 -0500 Subject: [PATCH 07/11] Allow PRs to push builds --- .../workflows/azure-static-web-apps-brave-stone-019bd1110.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/azure-static-web-apps-brave-stone-019bd1110.yml b/.github/workflows/azure-static-web-apps-brave-stone-019bd1110.yml index 1ebf338..4c88e8b 100644 --- a/.github/workflows/azure-static-web-apps-brave-stone-019bd1110.yml +++ b/.github/workflows/azure-static-web-apps-brave-stone-019bd1110.yml @@ -4,6 +4,8 @@ on: push: tags: - v*.** + pull_request: + branches: main jobs: build_and_deploy_job: From 5a423deb557d172bbfc4b931f229282f5629143c Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Wed, 8 May 2024 07:13:05 -0500 Subject: [PATCH 08/11] Remove unused import --- astro/src/business-logic/patronage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/astro/src/business-logic/patronage.ts b/astro/src/business-logic/patronage.ts index 40058fc..c284ed8 100644 --- a/astro/src/business-logic/patronage.ts +++ b/astro/src/business-logic/patronage.ts @@ -1,4 +1,3 @@ -import { date } from 'astro/zod'; import { parseISO, addDays } from 'date-fns'; export interface PatronageEvents { From 71afe93727fd819889c89647f3dc3ccee577e634 Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Wed, 8 May 2024 07:37:55 -0500 Subject: [PATCH 09/11] Improve layout --- astro/src/business-logic/patronage.ts | 11 ++++++++--- astro/src/components/patronage/point-in-time.tsx | 5 +++-- .../components/sets/documents/md-styles.module.css | 6 +++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/astro/src/business-logic/patronage.ts b/astro/src/business-logic/patronage.ts index c284ed8..95e6300 100644 --- a/astro/src/business-logic/patronage.ts +++ b/astro/src/business-logic/patronage.ts @@ -22,6 +22,11 @@ export type PatronageEntry = { patronageYTD: number; }; +export type PatronageSnapshot = { + date: Date; + details: Record; +} + function normalizeDate(date: Date | string): Date { return typeof date === 'string' ? parseISO(date) : date; } @@ -55,10 +60,10 @@ export class PatronageRecord { return this; } - getDetailsAt(date: Date | string): Record { + getDetailsAt(date: Date | string): PatronageSnapshot { date = normalizeDate(date); const results: Record = {}; - if (!this.events[0]) return results; + if (!this.events[0]) return { date, details: results }; let nextEvent: PatronageEvent | undefined; for (let current = this.events[0].date, nextEventIndex = 0; current <= date; current = addDays(current, 1)) { // reset from end of year @@ -105,7 +110,7 @@ export class PatronageRecord { const base = entry.tenureYTD + entry.patronageScheduleYTD; entry.patronageYTD = base + Math.min(entry.tenure, base * 2); } - return results; + return { date, details: results }; } get allEvents() { return [...this.events]; } diff --git a/astro/src/components/patronage/point-in-time.tsx b/astro/src/components/patronage/point-in-time.tsx index 824ee43..41fd9de 100644 --- a/astro/src/components/patronage/point-in-time.tsx +++ b/astro/src/components/patronage/point-in-time.tsx @@ -1,5 +1,5 @@ import { compareAsc, format } from 'date-fns'; -import type { PatronageEntry } from "../../business-logic/patronage"; +import type { PatronageSnapshot } from "../../business-logic/patronage"; const { amber, blue, cyan, emerald, orange, purple, rose } = (await import('tailwindcss/colors')).default; const colorIndices = [ @@ -26,11 +26,12 @@ const colors = (Array(colorIndices.length * colorMaps.length).fill(0).map((_, in return colorRange[colorIndices[index % colorIndices.length]!]; })) -export function PointInTime({ data, membersOrder }: { data: Record, membersOrder?: string[] }) { +export function PointInTime({ data: { date, details: data}, membersOrder }: { data: PatronageSnapshot; membersOrder?: string[] }) { const allEntries = Object.entries(data); const order = membersOrder ?? allEntries.sort((a, b) => compareAsc(a[1].memberDate, b[1].memberDate)).map(([name]) => name); return ( <> + {format(date, 'MMM d, yyyy')}
{order.map((name, index) =>
diff --git a/astro/src/components/sets/documents/md-styles.module.css b/astro/src/components/sets/documents/md-styles.module.css index 3a73330..8ecae8e 100644 --- a/astro/src/components/sets/documents/md-styles.module.css +++ b/astro/src/components/sets/documents/md-styles.module.css @@ -1,16 +1,16 @@ .container :global h1 { - @apply text-2xl font-bold; + @apply text-2xl font-bold mt-8 mb-4; font-variant: small-caps; } .container :global h2 { - @apply text-xl font-bold; + @apply text-xl font-bold mt-8 mb-4; font-variant: small-caps; } .container :global h3 { - @apply text-lg font-bold; + @apply text-lg font-bold mt-8 mb-4; font-variant: small-caps; } From 23b6538272e557748882ae62eecf3f7185fdf069 Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Thu, 9 May 2024 07:07:19 -0500 Subject: [PATCH 10/11] Add REI bylaws link --- Bylaws/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Bylaws/index.md b/Bylaws/index.md index 8888526..c0464c6 100644 --- a/Bylaws/index.md +++ b/Bylaws/index.md @@ -6,6 +6,7 @@ Sample](https://drive.google.com/file/d/0B7aFfmk4wQfbWVRPQzFkU1ZHLVE/view?resourcekey=0-Ldj_CiYRwRyAUyaGo6ThXg) Alternatives: +- [REI](https://www.rei.com/assets/about-rei/governance/rei-bylaws/live.pdf) - [Boston Techcollective Cooperative](https://drive.google.com/file/d/0B7aFfmk4wQfbbWxoYjNPVEVGbjg/view?resourcekey=0-ejmpEn33O9JxmiOXgF-tMA) - [Sample with Two Classes at page From b1d688ec75d2544d616f6a7519c3ac198ef9b487 Mon Sep 17 00:00:00 2001 From: Matt DeKrey Date: Thu, 9 May 2024 07:55:10 -0500 Subject: [PATCH 11/11] Expand tenure allocation logic --- astro/src/business-logic/patronage.ts | 35 +++++++++++-------- astro/src/content/documents/bylaws.mdx | 5 +++ .../stories/patronage/2-basic-example.mdx | 8 ++--- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/astro/src/business-logic/patronage.ts b/astro/src/business-logic/patronage.ts index 95e6300..8286f33 100644 --- a/astro/src/business-logic/patronage.ts +++ b/astro/src/business-logic/patronage.ts @@ -1,8 +1,17 @@ import { parseISO, addDays } from 'date-fns'; +// One per day of the week +type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6; +type TenureSchedule = Record; +export const noWorkSchedule: TenureSchedule = [0,0,0,0,0,0,0]; +// TODO: it's unclear in the bylaws when Tenure is allocated; what happens for +// partial weeks at beginning/end of the year? This code allows for a tenure +// schedule array. +export const fullWorkWeekSchedule: TenureSchedule = [0, 2, 1, 1, 1, 0, 0]; + export interface PatronageEvents { - addMember: { name: string; tenureRatePerWeek: number }; - tenureRate: { name: string; tenureRatePerWeek: number }; + addMember: { name: string; tenureSchedule: TenureSchedule }; + tenureRate: { name: string; tenureSchedule: TenureSchedule }; patronage: { name: string; additional: number; comment: string }; } @@ -15,7 +24,7 @@ export type PatronageEvent TODO: Make it clear that tenuer is earned at the completion of 1 hour, + > 8 hours, etc. rather than at the end of the week. + 8. "Patronage" shall be calculated per Member within a fiscal year as the sum of the following: @@ -810,6 +813,8 @@ https://drive.google.com/file/d/0B7aFfmk4wQfbWVRPQzFkU1ZHLVE/view?resourcekey=0- 2. Patronage Dividends may be by qualified or non-qualified written notices of allocation or a combination of the two. + > TODO: When are dividends allocated/distributed? + 6. Distributions of Interest on Member Accounts. The Cooperative may, by a decision of the Board, pay interest to Members on the Members Accounts. The interest may be paid in cash or as an additional credit to the Member diff --git a/astro/src/content/stories/patronage/2-basic-example.mdx b/astro/src/content/stories/patronage/2-basic-example.mdx index 9ba53b9..12d9fd6 100644 --- a/astro/src/content/stories/patronage/2-basic-example.mdx +++ b/astro/src/content/stories/patronage/2-basic-example.mdx @@ -4,13 +4,13 @@ description: A simple example on how Patronage is intended to work order: 10 --- -import { PatronageRecord } from '../../../business-logic/patronage'; +import { PatronageRecord, fullWorkWeekSchedule } from '../../../business-logic/patronage'; import { PointInTime } from '../../../components/patronage/point-in-time'; export const patronageExample = new PatronageRecord() - .add('2024-04-29', 'addMember', { name: 'Alfred', tenureRatePerWeek: 5 }) - .add('2024-07-29', 'addMember', { name: 'Betty', tenureRatePerWeek: 5 }) - .add('2024-12-30', 'addMember', { name: 'Caitlin', tenureRatePerWeek: 5 }) + .add('2024-04-29', 'addMember', { name: 'Alfred', tenureSchedule: fullWorkWeekSchedule }) + .add('2024-07-29', 'addMember', { name: 'Betty', tenureSchedule: fullWorkWeekSchedule }) + .add('2024-12-30', 'addMember', { name: 'Caitlin', tenureSchedule: fullWorkWeekSchedule }) As a simple example, let's consider the following events: