# Clubs - Dev Plan
## Overall planning / general takes pre-dev planning:
https://hackmd.io/KBdpVI6TTQ2y7rM5dcGeyg
## Data schema
```typescript=
model Link {
id Int @id @default(autoincrement())
url String
type LinkType
entityId Int
entityType String
}
```
```typescript=
enum Availibity {
Public
Private
}
```
```typescript=
model EntityAccess {
accessToId // The entity
accessToType
accessorId Int // The entity being granted access
accessorType String // The type of the entity being granted access
}
const entityAccessExample = {
accessToId: 'model1',
accessToType: 'model',
accessorId: 'club1',
accessorType: 'club'
}
```
```typescript=
model Club {
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
coverImageId Int?
coverImage Image? @relation(fields: [coverImageId], references: [id], onDelete: SetNull)
headerImageId Int?
headerImage Image? @relation(fields: [headerImageId], references: [id], onDelete: SetNull)
avatarId Int?
avatar Image? @relation(fields: [avatarId], references: [id], onDelete: SetNull)
name String
description String
nsfw Boolean @default(false)
billing Boolean @default(true)
unlisted Boolean @default(false)
buzzAccountId Int?
@@id([userId])
}
```
```typescript=
model ClubTier {
clubId Int
club Club @relation(fields: [clubId], references: [id], onDelete: Cascade)
unitAmount Int
currency Currency @default(Buzz)
name String
description String
coverImageId Int?
coverImage Image? @relation(fields: [coverImageId], references: [id], onDelete: SetNull)
// HOLD OFF COSMETICS
// cosmeticId Int?
// cosmetic Cosmetic? @relation(fields: [cosmeticId], references: [id], onDelete: SetNull)
// Whether or not this will be displayed in the common options for
// memberships.
unlisted Boolean @default(false)
// Can only be joined to via a club Admin adding the user.
joinable Boolean
}
```
```typescript=
enum ClubMembershipRole {
Admin // Can manage all content
Contributor // Can add and manage their own content
Member
}
```
```typescript=
// - Single row per-user per club.
model ClubMembership {
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
clubId Int
club Club @relation(fields: [clubId], ...)
clubTierId Int
clubTier ClubTier @relation(fields: [clubTierId], references: [id], onDelete: Cascade)
startedAt Date
expiresAt Date
cancelledAt Date?
role ClubMembershipRole @default(Member)
nextBillingAt Date
unitAmount Int
currency Currency @default(Buzz)
@@unique([userId, clubId])
}
// Track the
model ClubMembershipCharge {
userId Int
clubId Int
clubTierId Int
chargedAt Date
status String?
invoiceId (stripe)
unitAmount Int
unitAmountPurchased Int
currency Currency @default(Buzz)
}
```
```typescript=
model ClubEntity {
clubId Int
club Club
entityType String
entityId Int
addedById Int
addedBy User
membersOnly Boolean
@@id([clubId, entityId])
}
```
```typescript=
model Model {
...
// EntityAccess can be gotten from
availability Availability @default(Public)
unlisted Boolean @default(false)
...
}
```
```typescript=
model Article {
...
availability Availability @default(Public)
unlisted Boolean @default(false)
...
}
```
```typescript=
// Stretch 1
model Post {
...
availability Availability @default(Public)
unlisted Boolean @default(false)
...
}
```
```typescript=
// Stretch 2
model Bounty {
...
availability Availability @default(Public)
unlisted Boolean @default(false)
...
}
```
## Dataschema nits & bits
### EntityId/EntityType ✓
The main downside to using `entityId/entityType` approach is that we'll get no real type verification from Prisma and will end up doing some heavy SQL queries on our own, however, and if it serves as a proof of concept, one thing that I'd say helds back collections is the fact that this is not the approach taken.
We already have views in the future to add Bounties and posts, and it is 100% likely that more stuff will fall on (i.e, collections). Because of this, we need to think of making this flexible enough that we can rely on it.
### Club Buzz Account ✓
In the club schema, a `buzzAccountId` has been added. Although we could technically attempt to keep track of buzz on our end, it'd be ideal to use the existing buzz service to do this, as it already has too much built in we can probably re-use.
A good way to do this is to allow custom accountIds that can be tied to an owner/list of admins that can take from that pool. This needs discussion with Koen since he handles this service, but might be worth exploring.
If we go this route, this could work pretty much like a bank in which you open multiple accounts and different people have access to it and whatnot. It's a pattern proven to work.
~~Plan B is to just attempt to keep the value on our end + track transactions, but realistically, it might not be as full-fledged as the service and will mean to maintain 2 things that do the exact same job.~~
> [color=#45ef94] This has been discussed with Koen. [Check out the clickup task](https://app.clickup.com/t/8686g72w4)
### Image hash ✓
We already have image hash for BlurHash on NSWF <3 Use that to hide image information for users. No need to **EVER** download the image if a member has no access to it.
## General query changes:
### Models / Articles / Posts
- Should start returning listed ones on `GetInfinite`. Unlisted items may require permission to view and should not be listed in public grids.
- Should start checking for permission on `GetById`. If availability is **NOT** public, check `EntityAccess`.
### Images
- The image query is to be revised such that it does not expose non public posts onto the `getInfinite` view. Access must be checked if a postId is provided.
## Important considerations
### QA/Testing
Moving to a permission/access based back-end structure will require a more delicate permission revision per every request. A query may now be made before every operation to confirm whether the logged user has access to these items. This means that this feature is highly prone to little nits and overlooks, so it must be QAed carefully. This might also affect our API requests, as some items will now error out / not be returned without proper permission.
## TRPC Endpoints - Users
### `trpc.user.clubs`
Returns the clubs the user has created and/or those that they belong to. (Should be filterable).
## TRPC Endpoints - Clubs
### `trpc.club.getById` (Query) ✓
Returns a club by ID and it's details.
### `trpc.club.getItems` (Query) (AKA: Feed) ✓
Access to these items will still depend on the user's tier, but basic information about this item may be returned (i.e, model name, article title, etc).
### `trpc.club.join` (Mutation) ✓
Creates a new ClubMembership for the signed in user.
### `trpc.club.cancelMembership` (Mutation)
Cancels an existing membership. Note that canceling a membership will still leave access to the group until they the membership expires.
### `trpc.club.members` (Query)
Returns a list of existing members. Should only be available to club owner and admins.
### `trpc.club.tiers` (Query) ✓
Returns a list of existing tiers. If owner, return unlisted tiers.
### `trpc.club.upsertTier` (Mutation) ✓
Creates or updates a club tier.
### `trpc.club.addItem` (Mutation) ✓
Adds an existing item (article, model, etc) to a Club. Note that by doing this, the item will be marked as `Access = Club`. Only the owner of a item can add it to a club. They can only add it to clubs they own or are contributors on.
### `trpc.club.manageItems` (Query)
Allows the owner/admins/contributors of the club to manage the resources and access requirements (on a tier-basis). Owners/admins can manage all items. Contributors, can only manage items they own.
### `trpc.club.updateItems` (Mutation)
Sets new tier-access to items. Allows to remove items from the Club.
#### Sample input
```typescript=
const updateItemsSchema = z.object({
clubId: z.number(),
items: z.array(z.object({
entityId: z.number(),
entityType: z.string(),
clubTierIds: z.array(z.number())
})).optional(),
deleteItems: z.array(z.object({
entityId: z.number(),
entityType: z.string(),
})).optional(),
})
```
### `trpc.club.updateMember` (Mutation)
Allows an admin to forcefully change / update a member subscription tier on a club. Price can be lowered, but **not** increased. Should use the `unitAmount` on `ClubMembership`.
## TRPC Endpoints - Buzz
General refactor here to allow to pass a specific account Id. (must check that the user has permission to view that ID).
Some new endpoints will be exposed by Koen. [Check out the clickup task](https://app.clickup.com/t/8686g72w4)
**NOTE**: For V1, do not allow selecting the account on the buzz buttn. If a user wants to use "Club" money, they need to transfer funds to their own account.
### `trpc.buzz.ownedAccounts`
Returns a list of owned buzz accounts.
### `trpc.buzz.updateSettings`
Allows to setup default buzz account.
### `trpc.buzz.performTrasaction` (Exists)
Update to allow to use custom account.
### `trpc.buzz.transferBuzz`
Allows to transfer money from one owned account to another.
### Briant Notes
- Feed
- ClubPost
- Models
- Model
- Articles
- Article