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
+
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}
+
+ )}
+
+
+
+
+
+ Name |
+ Member Date |
+ Weeks YTD |
+ Tenure (YTD/total) |
+ Patronage Schedule (YTD) |
+ Patronage (YTD) |
+
+
+
+ {order.map((name, index) =>
+ {
+ const current = data[name]!;
+ return
+
+ {' '}{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: