create-protobase 0.23.13 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ask-preset.js +25 -0
- package/dist/create-project.js +7 -6
- package/dist/index.js +19 -9
- package/dist/next-steps.js +8 -2
- package/dist/presets.js +17 -0
- package/dist/templates/erp/.build-test-abf2a11f/protobase.config.ts +6 -0
- package/dist/templates/erp/.build-test-abf2a11f/resources.ts +2 -0
- package/dist/templates/erp/.build-test-b878b26f/config/index.ts +2 -0
- package/dist/templates/erp/.build-test-b878b26f/config/things/data.ts +2 -0
- package/dist/templates/erp/.build-test-b878b26f/config/things/ui.ts +2 -0
- package/dist/templates/erp/.env.example +14 -0
- package/dist/templates/erp/AGENTS.md +24 -0
- package/dist/templates/erp/auth/auth.ts +68 -0
- package/dist/templates/erp/components/orders-per-day.tsx +21 -0
- package/dist/templates/erp/config/bottom-bar.ts +9 -0
- package/dist/templates/erp/config/categories/data.ts +20 -0
- package/dist/templates/erp/config/categories/ui.ts +11 -0
- package/dist/templates/erp/config/companies/data.ts +23 -0
- package/dist/templates/erp/config/companies/ui.ts +22 -0
- package/dist/templates/erp/config/company_tags/data.ts +16 -0
- package/dist/templates/erp/config/company_tags/ui.ts +7 -0
- package/dist/templates/erp/config/countries/data.ts +13 -0
- package/dist/templates/erp/config/countries/ui.ts +6 -0
- package/dist/templates/erp/config/currencies/data.ts +14 -0
- package/dist/templates/erp/config/currencies/ui.ts +6 -0
- package/dist/templates/erp/config/emp_master/data.ts +19 -0
- package/dist/templates/erp/config/emp_master/ui.ts +7 -0
- package/dist/templates/erp/config/index.ts +42 -0
- package/dist/templates/erp/config/invoice_lines/data.ts +23 -0
- package/dist/templates/erp/config/invoice_lines/ui.ts +7 -0
- package/dist/templates/erp/config/invoices/data.ts +26 -0
- package/dist/templates/erp/config/invoices/ui.ts +38 -0
- package/dist/templates/erp/config/jobs.test.ts +23 -0
- package/dist/templates/erp/config/jobs.ts +29 -0
- package/dist/templates/erp/config/locations/data.ts +18 -0
- package/dist/templates/erp/config/locations/ui.ts +6 -0
- package/dist/templates/erp/config/order_lines/data.ts +20 -0
- package/dist/templates/erp/config/order_lines/ui.ts +7 -0
- package/dist/templates/erp/config/orders/data.ts +27 -0
- package/dist/templates/erp/config/orders/ui.ts +41 -0
- package/dist/templates/erp/config/organizations/data.ts +17 -0
- package/dist/templates/erp/config/organizations/ui.ts +6 -0
- package/dist/templates/erp/config/overview/page.tsx +80 -0
- package/dist/templates/erp/config/pages.test.ts +14 -0
- package/dist/templates/erp/config/people/data.ts +21 -0
- package/dist/templates/erp/config/people/ui.ts +6 -0
- package/dist/templates/erp/config/products/data.ts +25 -0
- package/dist/templates/erp/config/products/ui.ts +6 -0
- package/dist/templates/erp/config/roles.test.ts +56 -0
- package/dist/templates/erp/config/roles.ts +54 -0
- package/dist/templates/erp/config/shell.test.ts +24 -0
- package/dist/templates/erp/config/stock_levels/data.ts +16 -0
- package/dist/templates/erp/config/stock_levels/ui.ts +6 -0
- package/dist/templates/erp/config/stock_moves/data.ts +22 -0
- package/dist/templates/erp/config/stock_moves/ui.ts +25 -0
- package/dist/templates/erp/config/tags/data.ts +17 -0
- package/dist/templates/erp/config/tags/ui.ts +6 -0
- package/dist/templates/erp/config/user-menu.ts +9 -0
- package/dist/templates/erp/config/users/data.ts +19 -0
- package/dist/templates/erp/config/users/ui.ts +9 -0
- package/dist/templates/erp/config/warehouses/data.ts +19 -0
- package/dist/templates/erp/config/warehouses/ui.ts +6 -0
- package/dist/templates/erp/data.ts +3 -0
- package/dist/templates/erp/db/connection.ts +7 -0
- package/dist/templates/erp/db/drop.ts +9 -0
- package/dist/templates/erp/db/kysely.ts +13 -0
- package/dist/templates/erp/db/migrations/001_core.sql +48 -0
- package/dist/templates/erp/db/migrations/002_crm.sql +59 -0
- package/dist/templates/erp/db/migrations/003_catalog.sql +38 -0
- package/dist/templates/erp/db/migrations/004_sales.sql +86 -0
- package/dist/templates/erp/db/migrations/005_inventory.sql +65 -0
- package/dist/templates/erp/db/migrations/006_hr_legacy.sql +16 -0
- package/dist/templates/erp/db/migrations/007_filter_indexes.sql +4 -0
- package/dist/templates/erp/db/migrations/008_calculated_totals.sql +98 -0
- package/dist/templates/erp/db/migrations/011_auth_organizations.sql +22 -0
- package/dist/templates/erp/db/migrations/012_jobs.sql +531 -0
- package/dist/templates/erp/db/migrations/013_jobs.sql +98 -0
- package/dist/templates/erp/db/migrations/016_jobs.sql +99 -0
- package/dist/templates/erp/db/migrations/017_filter_indexes.sql +5 -0
- package/dist/templates/erp/db/migrations/018_sort_indexes.sql +4 -0
- package/dist/templates/erp/db/migrations/019_auth_users.sql +18 -0
- package/dist/templates/erp/functions/unpaid-orders.ts +13 -0
- package/dist/templates/erp/package.json +38 -0
- package/dist/templates/erp/protobase.config.ts +22 -0
- package/dist/templates/erp/protobase.ui.tsx +14 -0
- package/dist/templates/erp/seed/auth-organizations.ts +11 -0
- package/dist/templates/erp/seed/base-organization.test.ts +17 -0
- package/dist/templates/erp/seed/base-organization.ts +10 -0
- package/dist/templates/erp/seed/base.ts +22 -0
- package/dist/templates/erp/seed/builders/categories.ts +18 -0
- package/dist/templates/erp/seed/builders/company.ts +61 -0
- package/dist/templates/erp/seed/builders/invoice.ts +31 -0
- package/dist/templates/erp/seed/builders/order.ts +66 -0
- package/dist/templates/erp/seed/builders/product.ts +46 -0
- package/dist/templates/erp/seed/calendar.ts +60 -0
- package/dist/templates/erp/seed/cli.ts +54 -0
- package/dist/templates/erp/seed/copy.ts +22 -0
- package/dist/templates/erp/seed/data/catalogTree.ts +72 -0
- package/dist/templates/erp/seed/data/layout.ts +19 -0
- package/dist/templates/erp/seed/data/names.ts +44 -0
- package/dist/templates/erp/seed/data/reference.ts +53 -0
- package/dist/templates/erp/seed/format.ts +24 -0
- package/dist/templates/erp/seed/maintenance.ts +63 -0
- package/dist/templates/erp/seed/rng.ts +36 -0
- package/dist/templates/erp/seed/scales.ts +9 -0
- package/dist/templates/erp/seed/steps/catalog.ts +34 -0
- package/dist/templates/erp/seed/steps/crm.ts +62 -0
- package/dist/templates/erp/seed/steps/hr.ts +26 -0
- package/dist/templates/erp/seed/steps/ledger.ts +88 -0
- package/dist/templates/erp/seed/steps/reference.ts +24 -0
- package/dist/templates/erp/seed/steps/sales.ts +58 -0
- package/dist/templates/erp/seed/steps/warehouses.ts +42 -0
- package/dist/templates/erp/seed/world.ts +86 -0
- package/dist/templates/erp/vitest.config.ts +4 -0
- package/dist/templates/real-estate/.env.example +9 -0
- package/dist/templates/real-estate/AGENTS.md +24 -0
- package/dist/templates/real-estate/_.gitignore +3 -0
- package/dist/templates/real-estate/_bunfig.toml +4 -0
- package/dist/templates/real-estate/auth/auth.ts +36 -0
- package/dist/templates/real-estate/config/amenities/data.ts +12 -0
- package/dist/templates/real-estate/config/amenities/ui.ts +7 -0
- package/dist/templates/real-estate/config/arrears/data.ts +19 -0
- package/dist/templates/real-estate/config/arrears/ui.ts +21 -0
- package/dist/templates/real-estate/config/deposits/data.ts +21 -0
- package/dist/templates/real-estate/config/deposits/ui.ts +17 -0
- package/dist/templates/real-estate/config/index.ts +41 -0
- package/dist/templates/real-estate/config/inspections/data.ts +22 -0
- package/dist/templates/real-estate/config/inspections/ui.ts +15 -0
- package/dist/templates/real-estate/config/lease_tenants/data.ts +16 -0
- package/dist/templates/real-estate/config/lease_tenants/ui.ts +8 -0
- package/dist/templates/real-estate/config/leases/data.ts +29 -0
- package/dist/templates/real-estate/config/leases/ui.ts +33 -0
- package/dist/templates/real-estate/config/organizations/data.ts +17 -0
- package/dist/templates/real-estate/config/organizations/ui.ts +7 -0
- package/dist/templates/real-estate/config/overview/page.tsx +57 -0
- package/dist/templates/real-estate/config/owners/data.ts +22 -0
- package/dist/templates/real-estate/config/owners/ui.ts +17 -0
- package/dist/templates/real-estate/config/pages.test.ts +49 -0
- package/dist/templates/real-estate/config/payments/data.ts +20 -0
- package/dist/templates/real-estate/config/payments/ui.ts +14 -0
- package/dist/templates/real-estate/config/properties/data.ts +26 -0
- package/dist/templates/real-estate/config/properties/ui.ts +21 -0
- package/dist/templates/real-estate/config/rent_charges/data.ts +22 -0
- package/dist/templates/real-estate/config/rent_charges/ui.ts +27 -0
- package/dist/templates/real-estate/config/roles.test.ts +49 -0
- package/dist/templates/real-estate/config/roles.ts +45 -0
- package/dist/templates/real-estate/config/shell.test.ts +18 -0
- package/dist/templates/real-estate/config/tenants/data.ts +23 -0
- package/dist/templates/real-estate/config/tenants/ui.ts +27 -0
- package/dist/templates/real-estate/config/tickets/data.ts +33 -0
- package/dist/templates/real-estate/config/tickets/ui.ts +58 -0
- package/dist/templates/real-estate/config/unit_amenities/data.ts +15 -0
- package/dist/templates/real-estate/config/unit_amenities/ui.ts +8 -0
- package/dist/templates/real-estate/config/units/data.ts +24 -0
- package/dist/templates/real-estate/config/units/ui.ts +41 -0
- package/dist/templates/real-estate/config/user-menu.ts +9 -0
- package/dist/templates/real-estate/config/users/data.ts +19 -0
- package/dist/templates/real-estate/config/users/ui.ts +11 -0
- package/dist/templates/real-estate/config/valuations/data.ts +19 -0
- package/dist/templates/real-estate/config/valuations/ui.ts +13 -0
- package/dist/templates/real-estate/config/vendors/data.ts +21 -0
- package/dist/templates/real-estate/config/vendors/ui.ts +18 -0
- package/dist/templates/real-estate/config/work_orders/data.ts +22 -0
- package/dist/templates/real-estate/config/work_orders/ui.ts +18 -0
- package/dist/templates/real-estate/db/connection.ts +7 -0
- package/dist/templates/real-estate/db/drop.ts +9 -0
- package/dist/templates/real-estate/db/kysely.ts +13 -0
- package/dist/templates/real-estate/db/migrate.ts +30 -0
- package/dist/templates/real-estate/db/migrations/001_core.sql +33 -0
- package/dist/templates/real-estate/db/migrations/002_portfolio.sql +97 -0
- package/dist/templates/real-estate/db/migrations/003_leasing.sql +78 -0
- package/dist/templates/real-estate/db/migrations/004_billing.sql +88 -0
- package/dist/templates/real-estate/db/migrations/005_maintenance.sql +105 -0
- package/dist/templates/real-estate/db/migrations/006_outstanding.sql +14 -0
- package/dist/templates/real-estate/db/migrations/007_auth.sql +36 -0
- package/dist/templates/real-estate/db/migrations/008_auth_snake_case.sql +109 -0
- package/dist/templates/real-estate/db/migrations/009_auth.sql +6 -0
- package/dist/templates/real-estate/db/migrations/010_auth.sql +4 -0
- package/dist/templates/real-estate/db/migrations/011_filter_indexes.sql +8 -0
- package/dist/templates/real-estate/db/migrations/012_sort_indexes.sql +2 -0
- package/dist/templates/real-estate/db/migrations/013_auth_users.sql +17 -0
- package/dist/templates/real-estate/node-types.d.ts +1 -0
- package/dist/templates/real-estate/package.json +38 -0
- package/dist/templates/real-estate/protobase.config.ts +5 -0
- package/dist/templates/real-estate/protobase.ui.tsx +16 -0
- package/dist/templates/real-estate/seed/base-organization.test.ts +17 -0
- package/dist/templates/real-estate/seed/base-organization.ts +10 -0
- package/dist/templates/real-estate/seed/base.ts +18 -0
- package/dist/templates/real-estate/seed/builders/allocation.ts +47 -0
- package/dist/templates/real-estate/seed/builders/ledger.ts +52 -0
- package/dist/templates/real-estate/seed/builders/property.ts +98 -0
- package/dist/templates/real-estate/seed/builders/tenancy.ts +99 -0
- package/dist/templates/real-estate/seed/builders/ticket.ts +115 -0
- package/dist/templates/real-estate/seed/calendar.ts +77 -0
- package/dist/templates/real-estate/seed/cli.ts +50 -0
- package/dist/templates/real-estate/seed/copy.ts +22 -0
- package/dist/templates/real-estate/seed/data/names.ts +31 -0
- package/dist/templates/real-estate/seed/data/reference.ts +63 -0
- package/dist/templates/real-estate/seed/format.ts +24 -0
- package/dist/templates/real-estate/seed/iban.ts +15 -0
- package/dist/templates/real-estate/seed/maintenance.ts +63 -0
- package/dist/templates/real-estate/seed/rng.ts +36 -0
- package/dist/templates/real-estate/seed/scales.ts +9 -0
- package/dist/templates/real-estate/seed/steps/billing.ts +44 -0
- package/dist/templates/real-estate/seed/steps/leasing.ts +60 -0
- package/dist/templates/real-estate/seed/steps/maintenance.ts +77 -0
- package/dist/templates/real-estate/seed/steps/portfolio.ts +97 -0
- package/dist/templates/real-estate/seed/steps/reference.ts +21 -0
- package/dist/templates/real-estate/seed/world.ts +92 -0
- package/dist/templates/real-estate/tsconfig.json +16 -0
- package/dist/templates/real-estate/vitest.config.ts +4 -0
- package/dist/templates/scratch/_.gitignore +3 -0
- package/dist/templates/scratch/_bunfig.toml +4 -0
- package/dist/templates/scratch/db/migrate.ts +30 -0
- package/dist/templates/scratch/db/migrations/001_auth.sql +36 -0
- package/dist/templates/scratch/db/migrations/002_auth_snake_case.sql +109 -0
- package/dist/templates/scratch/db/migrations/003_auth.sql +6 -0
- package/dist/templates/scratch/db/migrations/004_auth.sql +4 -0
- package/dist/templates/scratch/node-types.d.ts +1 -0
- package/dist/{template → templates/scratch}/package.json +5 -5
- package/dist/templates/scratch/tsconfig.json +16 -0
- package/package.json +2 -2
- /package/dist/{template → templates/erp}/_.gitignore +0 -0
- /package/dist/{template → templates/erp}/_bunfig.toml +0 -0
- /package/dist/{template → templates/erp}/db/migrate.ts +0 -0
- /package/dist/{template/db/migrations/001_auth.sql → templates/erp/db/migrations/009_auth.sql} +0 -0
- /package/dist/{template/db/migrations/002_auth_snake_case.sql → templates/erp/db/migrations/010_auth_snake_case.sql} +0 -0
- /package/dist/{template/db/migrations/003_auth.sql → templates/erp/db/migrations/014_auth.sql} +0 -0
- /package/dist/{template/db/migrations/004_auth.sql → templates/erp/db/migrations/015_auth.sql} +0 -0
- /package/dist/{template → templates/erp}/node-types.d.ts +0 -0
- /package/dist/{template → templates/erp}/tsconfig.json +0 -0
- /package/dist/{template → templates/scratch}/.env.example +0 -0
- /package/dist/{template → templates/scratch}/AGENTS.md +0 -0
- /package/dist/{template → templates/scratch}/auth/auth.ts +0 -0
- /package/dist/{template → templates/scratch}/config/index.ts +0 -0
- /package/dist/{template → templates/scratch}/config/user-menu.ts +0 -0
- /package/dist/{template → templates/scratch}/db/connection.ts +0 -0
- /package/dist/{template → templates/scratch}/db/migrations/.gitkeep +0 -0
- /package/dist/{template → templates/scratch}/protobase.config.ts +0 -0
- /package/dist/{template → templates/scratch}/vitest.config.ts +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { f, resource } from '@protobase/schema'
|
|
2
|
+
import { access } from '../roles'
|
|
3
|
+
|
|
4
|
+
/** maintenance.vendors: the contractors who carry out work orders. */
|
|
5
|
+
export const vendors = resource('vendors')
|
|
6
|
+
.table('maintenance.vendors')
|
|
7
|
+
.fields({
|
|
8
|
+
id: f.integer().readOnly().filterable().sortable().dbDefault(),
|
|
9
|
+
organization_id: f.relation('organizations').filterable().sortable(),
|
|
10
|
+
name: f.text(),
|
|
11
|
+
trade: f.enum(['plumbing', 'heating', 'electrical', 'roofing', 'carpentry', 'locksmith', 'pest_control', 'general']).filterable().sortable(),
|
|
12
|
+
email: f.text().optional(),
|
|
13
|
+
phone: f.text().optional(),
|
|
14
|
+
hourly_rate: f.decimal({ precision: 8, scale: 2 }),
|
|
15
|
+
active: f.boolean().default(true),
|
|
16
|
+
created_at: f.timestamp().readOnly().dbDefault(),
|
|
17
|
+
updated_at: f.timestamp().readOnly().dbDefault(),
|
|
18
|
+
})
|
|
19
|
+
.primaryKey((r) => r.id)
|
|
20
|
+
.tenant((r) => r.organization_id)
|
|
21
|
+
.access(access('vendors'))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { view } from '@protobase/schema'
|
|
2
|
+
import type { vendors } from './data'
|
|
3
|
+
|
|
4
|
+
export const vendorsView = view<typeof vendors>('vendors')
|
|
5
|
+
.title((r) => r.name)
|
|
6
|
+
.names({ singular: 'Vendor', plural: 'Vendors' })
|
|
7
|
+
.fields((r) => ({ trade: r.trade.format('badge').valueLabels({
|
|
8
|
+
plumbing: 'Plumbing',
|
|
9
|
+
heating: 'Heating',
|
|
10
|
+
electrical: 'Electrical',
|
|
11
|
+
roofing: 'Roofing',
|
|
12
|
+
carpentry: 'Carpentry',
|
|
13
|
+
locksmith: 'Locksmith',
|
|
14
|
+
pest_control: 'Pest control',
|
|
15
|
+
general: 'General',
|
|
16
|
+
}), hourly_rate: r.hourly_rate.prefix('€').decimals(2) }))
|
|
17
|
+
.list((r) => ({ columns: [r.name, r.trade, r.phone, r.hourly_rate, r.active] }))
|
|
18
|
+
.filters((r, w) => [w.facets(r.trade)])
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { f, resource } from '@protobase/schema'
|
|
2
|
+
import { access } from '../roles'
|
|
3
|
+
|
|
4
|
+
/** maintenance.work_orders: a vendor's visit for a ticket. */
|
|
5
|
+
export const workOrders = resource('work_orders')
|
|
6
|
+
.table('maintenance.work_orders')
|
|
7
|
+
.fields({
|
|
8
|
+
id: f.bigint().readOnly().filterable().sortable().dbDefault(),
|
|
9
|
+
organization_id: f.relation('organizations').filterable().sortable(),
|
|
10
|
+
ticket_id: f.relation('tickets').filterable().sortable(),
|
|
11
|
+
vendor_id: f.relation('vendors').filterable().sortable(),
|
|
12
|
+
scheduled_for: f.date().filterable().sortable(),
|
|
13
|
+
completed_on: f.date().optional(),
|
|
14
|
+
hours: f.decimal({ precision: 5, scale: 2 }).optional(),
|
|
15
|
+
cost: f.decimal({ precision: 10, scale: 2 }).optional(),
|
|
16
|
+
notes: f.text().optional(),
|
|
17
|
+
created_at: f.timestamp().readOnly().dbDefault(),
|
|
18
|
+
updated_at: f.timestamp().readOnly().dbDefault(),
|
|
19
|
+
})
|
|
20
|
+
.primaryKey((r) => r.id)
|
|
21
|
+
.tenant((r) => r.organization_id)
|
|
22
|
+
.access(access('work_orders'))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { l, view } from '@protobase/schema'
|
|
2
|
+
import type { workOrders } from './data'
|
|
3
|
+
|
|
4
|
+
export const workOrdersView = view<typeof workOrders>('work_orders')
|
|
5
|
+
.names({ singular: 'Work order', plural: 'Work orders' })
|
|
6
|
+
.fields((r) => ({
|
|
7
|
+
ticket_id: r.ticket_id.label('Ticket'),
|
|
8
|
+
vendor_id: r.vendor_id.label('Vendor'),
|
|
9
|
+
scheduled_for: r.scheduled_for.label('Visit'),
|
|
10
|
+
completed_on: r.completed_on.label('Completed'),
|
|
11
|
+
cost: r.cost.help('Labour and materials, as invoiced by the vendor.').prefix('€').decimals(2),
|
|
12
|
+
}))
|
|
13
|
+
.list((r) => ({ columns: [r.ticket_id, r.vendor_id, r.scheduled_for, r.completed_on, r.cost], sort: [[r.scheduled_for, 'desc']] }))
|
|
14
|
+
.filters((r, w) => [w.facets(r.vendor_id), w.dateRange(r.scheduled_for, { presets: ['7d', '30d', '90d'] })])
|
|
15
|
+
.layout((r) => [
|
|
16
|
+
l.section('Visit', [r.ticket_id, r.vendor_id, r.scheduled_for, r.completed_on]),
|
|
17
|
+
l.section('Invoice', [r.hours, r.cost, r.notes]),
|
|
18
|
+
])
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import postgres from 'postgres'
|
|
2
|
+
|
|
3
|
+
export const databaseUrl = process.env.DATABASE_URL ?? 'postgres://protobase:protobase@localhost:55432/real_estate'
|
|
4
|
+
|
|
5
|
+
export const connect = (options: postgres.Options<{}> = {}) => postgres(databaseUrl, { onnotice: () => {}, ...options })
|
|
6
|
+
|
|
7
|
+
export type Db = ReturnType<typeof connect>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { connect } from './connection'
|
|
2
|
+
|
|
3
|
+
const sql = connect({ max: 1 })
|
|
4
|
+
|
|
5
|
+
await sql.unsafe('drop schema if exists core, portfolio, leasing, billing, maintenance cascade')
|
|
6
|
+
await sql.unsafe('drop table if exists public.schema_migrations, public.seed_info')
|
|
7
|
+
console.log('dropped all real estate schemas')
|
|
8
|
+
|
|
9
|
+
await sql.end()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Kysely, PostgresDialect } from 'kysely'
|
|
2
|
+
import pg from 'pg'
|
|
3
|
+
import { databaseUrl } from './connection'
|
|
4
|
+
|
|
5
|
+
// date and timestamp (without zone) stay text; the API formats them itself
|
|
6
|
+
const types = {
|
|
7
|
+
getTypeParser: (oid: number, format?: 'text' | 'binary') =>
|
|
8
|
+
oid === 1082 || oid === 1114 ? (value: string) => value : pg.types.getTypeParser(oid, format as 'text'),
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Kysely over node-postgres, the way a deployment of the API would connect. */
|
|
12
|
+
export const createDb = (max = 10) =>
|
|
13
|
+
new Kysely<any>({ dialect: new PostgresDialect({ pool: new pg.Pool({ connectionString: databaseUrl, max, types }) }) })
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { connect } from './connection'
|
|
4
|
+
|
|
5
|
+
// Applies the migrations not applied yet, the app's and the auth schema's alike, in file name order and in one
|
|
6
|
+
// transaction: when one fails, none is applied and the database stays as the running version expects it. Run it before
|
|
7
|
+
// a new version serves, while nothing else writes. A statement Postgres runs only outside a transaction, such as
|
|
8
|
+
// `create index concurrently`, does not belong in these files.
|
|
9
|
+
|
|
10
|
+
const directory = fileURLToPath(new URL('./migrations/', import.meta.url))
|
|
11
|
+
// Any constant shared by every run of this script: two runs at once take turns.
|
|
12
|
+
const lockKey = 7_311_946_202
|
|
13
|
+
|
|
14
|
+
const sql = connect({ max: 1 })
|
|
15
|
+
const files = (await readdir(directory)).filter((file) => file.endsWith('.sql')).sort()
|
|
16
|
+
|
|
17
|
+
const applied = await sql.begin(async (tx) => {
|
|
18
|
+
await tx`select pg_advisory_xact_lock(${lockKey})`
|
|
19
|
+
await tx`create table if not exists public.schema_migrations (name text primary key, applied_at timestamptz not null default now())`
|
|
20
|
+
const done = new Set((await tx`select name from public.schema_migrations`).map((row) => row.name))
|
|
21
|
+
const pending = files.filter((file) => !done.has(file))
|
|
22
|
+
for (const file of pending) {
|
|
23
|
+
await tx.unsafe(await readFile(directory + file, 'utf8'))
|
|
24
|
+
await tx`insert into public.schema_migrations (name) values (${file})`
|
|
25
|
+
}
|
|
26
|
+
return pending
|
|
27
|
+
})
|
|
28
|
+
for (const file of applied) console.log(`applied ${file}`)
|
|
29
|
+
|
|
30
|
+
await sql.end()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
create extension if not exists pg_trgm;
|
|
2
|
+
create extension if not exists btree_gist;
|
|
3
|
+
|
|
4
|
+
create table public.seed_info (
|
|
5
|
+
scale text not null,
|
|
6
|
+
seeded_at timestamptz not null default now()
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
create schema core;
|
|
10
|
+
|
|
11
|
+
create table core.organizations (
|
|
12
|
+
id integer generated by default as identity primary key,
|
|
13
|
+
name text not null,
|
|
14
|
+
slug text not null unique,
|
|
15
|
+
city text not null,
|
|
16
|
+
kvk_number text not null,
|
|
17
|
+
created_at timestamptz not null default now(),
|
|
18
|
+
updated_at timestamptz not null default now()
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
create table core.users (
|
|
22
|
+
id integer generated by default as identity primary key,
|
|
23
|
+
organization_id integer not null references core.organizations (id),
|
|
24
|
+
email text not null,
|
|
25
|
+
name text not null,
|
|
26
|
+
role text not null check (role in ('admin', 'manager', 'finance', 'maintenance')),
|
|
27
|
+
active boolean not null default true,
|
|
28
|
+
created_at timestamptz not null default now(),
|
|
29
|
+
updated_at timestamptz not null default now(),
|
|
30
|
+
unique (organization_id, email)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
create index users_role_idx on core.users (organization_id, role);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
create schema portfolio;
|
|
2
|
+
|
|
3
|
+
create table portfolio.amenities (
|
|
4
|
+
code text primary key,
|
|
5
|
+
name text not null
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
create table portfolio.owners (
|
|
9
|
+
id bigint generated by default as identity primary key,
|
|
10
|
+
organization_id integer not null references core.organizations (id),
|
|
11
|
+
name text not null,
|
|
12
|
+
kind text not null check (kind in ('private', 'company', 'pension_fund', 'housing_association')),
|
|
13
|
+
email text,
|
|
14
|
+
phone text,
|
|
15
|
+
iban text not null,
|
|
16
|
+
management_fee_percent numeric(4, 2) not null default 6 check (management_fee_percent between 0 and 20),
|
|
17
|
+
created_at timestamptz not null default now(),
|
|
18
|
+
updated_at timestamptz not null default now()
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
create index owners_organization_idx on portfolio.owners (organization_id, created_at);
|
|
22
|
+
create index owners_kind_idx on portfolio.owners (organization_id, kind);
|
|
23
|
+
create index owners_name_trgm_idx on portfolio.owners using gin (name gin_trgm_ops);
|
|
24
|
+
|
|
25
|
+
create table portfolio.properties (
|
|
26
|
+
id bigint generated by default as identity primary key,
|
|
27
|
+
organization_id integer not null references core.organizations (id),
|
|
28
|
+
owner_id bigint not null references portfolio.owners (id),
|
|
29
|
+
name text not null,
|
|
30
|
+
kind text not null check (kind in ('apartment_building', 'single_family', 'mixed_use')),
|
|
31
|
+
street text not null,
|
|
32
|
+
house_number text not null,
|
|
33
|
+
postal_code text not null check (postal_code ~ '^[1-9][0-9]{3} [A-Z]{2}$'),
|
|
34
|
+
city text not null,
|
|
35
|
+
latitude numeric(8, 6) not null check (latitude between -90 and 90),
|
|
36
|
+
longitude numeric(9, 6) not null check (longitude between -180 and 180),
|
|
37
|
+
built_year smallint not null,
|
|
38
|
+
created_at timestamptz not null default now(),
|
|
39
|
+
updated_at timestamptz not null default now()
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
create index properties_organization_idx on portfolio.properties (organization_id, created_at);
|
|
43
|
+
create index properties_owner_idx on portfolio.properties (owner_id);
|
|
44
|
+
create index properties_city_idx on portfolio.properties (organization_id, city);
|
|
45
|
+
create index properties_kind_idx on portfolio.properties (organization_id, kind);
|
|
46
|
+
create index properties_location_idx on portfolio.properties (latitude, longitude);
|
|
47
|
+
create index properties_name_trgm_idx on portfolio.properties using gin (name gin_trgm_ops);
|
|
48
|
+
|
|
49
|
+
create table portfolio.units (
|
|
50
|
+
id bigint generated by default as identity primary key,
|
|
51
|
+
organization_id integer not null references core.organizations (id),
|
|
52
|
+
property_id bigint not null references portfolio.properties (id),
|
|
53
|
+
label text not null,
|
|
54
|
+
floor smallint not null default 0,
|
|
55
|
+
bedrooms smallint not null check (bedrooms >= 0),
|
|
56
|
+
area_m2 integer not null check (area_m2 > 0),
|
|
57
|
+
energy_label text not null check (energy_label in ('A++++', 'A+++', 'A++', 'A+', 'A', 'B', 'C', 'D', 'E', 'F', 'G')),
|
|
58
|
+
base_rent numeric(10, 2) not null check (base_rent > 0),
|
|
59
|
+
status text not null default 'vacant' check (status in ('let', 'vacant', 'renovation')),
|
|
60
|
+
created_at timestamptz not null default now(),
|
|
61
|
+
updated_at timestamptz not null default now(),
|
|
62
|
+
unique (property_id, label)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
create index units_organization_idx on portfolio.units (organization_id, created_at);
|
|
66
|
+
create index units_status_idx on portfolio.units (organization_id, status);
|
|
67
|
+
create index units_rent_idx on portfolio.units (organization_id, base_rent);
|
|
68
|
+
create index units_energy_label_idx on portfolio.units (organization_id, energy_label);
|
|
69
|
+
|
|
70
|
+
create table portfolio.unit_amenities (
|
|
71
|
+
unit_id bigint not null references portfolio.units (id),
|
|
72
|
+
amenity_code text not null references portfolio.amenities (code),
|
|
73
|
+
organization_id integer not null references core.organizations (id),
|
|
74
|
+
added_at timestamptz not null default now(),
|
|
75
|
+
primary key (unit_id, amenity_code)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
create index unit_amenities_amenity_idx on portfolio.unit_amenities (amenity_code);
|
|
79
|
+
create index unit_amenities_organization_idx on portfolio.unit_amenities (organization_id);
|
|
80
|
+
|
|
81
|
+
-- Valuations over time: the yearly WOZ value every property gets, and the odd appraisal.
|
|
82
|
+
create table portfolio.valuations (
|
|
83
|
+
id bigint generated by default as identity primary key,
|
|
84
|
+
organization_id integer not null references core.organizations (id),
|
|
85
|
+
property_id bigint not null references portfolio.properties (id),
|
|
86
|
+
valued_on date not null,
|
|
87
|
+
value numeric(14, 2) not null check (value > 0),
|
|
88
|
+
source text not null check (source in ('woz', 'appraisal')),
|
|
89
|
+
created_at timestamptz not null default now(),
|
|
90
|
+
updated_at timestamptz not null default now(),
|
|
91
|
+
unique (property_id, source, valued_on)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
create index valuations_organization_idx on portfolio.valuations (organization_id, valued_on);
|
|
95
|
+
create index valuations_source_idx on portfolio.valuations (organization_id, source);
|
|
96
|
+
|
|
97
|
+
create index units_label_trgm_idx on portfolio.units using gin (label gin_trgm_ops);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
create schema leasing;
|
|
2
|
+
|
|
3
|
+
create table leasing.tenants (
|
|
4
|
+
id bigint generated by default as identity primary key,
|
|
5
|
+
organization_id integer not null references core.organizations (id),
|
|
6
|
+
first_name text not null,
|
|
7
|
+
last_name text not null,
|
|
8
|
+
email text not null,
|
|
9
|
+
phone text,
|
|
10
|
+
date_of_birth date not null,
|
|
11
|
+
iban text not null,
|
|
12
|
+
monthly_income numeric(10, 2) not null check (monthly_income >= 0),
|
|
13
|
+
created_at timestamptz not null default now(),
|
|
14
|
+
updated_at timestamptz not null default now()
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
create index tenants_organization_idx on leasing.tenants (organization_id, created_at);
|
|
18
|
+
create index tenants_last_name_trgm_idx on leasing.tenants using gin (last_name gin_trgm_ops);
|
|
19
|
+
create index tenants_email_trgm_idx on leasing.tenants using gin (email gin_trgm_ops);
|
|
20
|
+
|
|
21
|
+
-- end_date is the last day of the tenancy, null while it runs for an indefinite term.
|
|
22
|
+
-- Two leases of one unit never overlap: Postgres refuses the second with an exclusion constraint.
|
|
23
|
+
create table leasing.leases (
|
|
24
|
+
id bigint generated by default as identity primary key,
|
|
25
|
+
organization_id integer not null references core.organizations (id),
|
|
26
|
+
number text not null,
|
|
27
|
+
unit_id bigint not null references portfolio.units (id),
|
|
28
|
+
term text not null check (term in ('indefinite', 'fixed')),
|
|
29
|
+
start_date date not null,
|
|
30
|
+
end_date date,
|
|
31
|
+
monthly_rent numeric(10, 2) not null check (monthly_rent > 0),
|
|
32
|
+
service_costs numeric(10, 2) not null default 0 check (service_costs >= 0),
|
|
33
|
+
notice_given_on date,
|
|
34
|
+
created_at timestamptz not null default now(),
|
|
35
|
+
updated_at timestamptz not null default now(),
|
|
36
|
+
unique (organization_id, number),
|
|
37
|
+
check (end_date is null or end_date >= start_date),
|
|
38
|
+
check (term = 'indefinite' or end_date is not null),
|
|
39
|
+
constraint leases_no_overlap exclude using gist (unit_id with =, daterange(start_date, end_date, '[]') with &&)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
create index leases_organization_idx on leasing.leases (organization_id, created_at);
|
|
43
|
+
create index leases_unit_idx on leasing.leases (unit_id);
|
|
44
|
+
create index leases_start_idx on leasing.leases (organization_id, start_date);
|
|
45
|
+
create index leases_end_idx on leasing.leases (organization_id, end_date);
|
|
46
|
+
|
|
47
|
+
-- Everyone who signed a lease: one primary tenant, and co-signers.
|
|
48
|
+
create table leasing.lease_tenants (
|
|
49
|
+
lease_id bigint not null references leasing.leases (id),
|
|
50
|
+
tenant_id bigint not null references leasing.tenants (id),
|
|
51
|
+
organization_id integer not null references core.organizations (id),
|
|
52
|
+
role text not null check (role in ('primary', 'co_signer')),
|
|
53
|
+
added_at timestamptz not null default now(),
|
|
54
|
+
primary key (lease_id, tenant_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
create unique index lease_tenants_primary_idx on leasing.lease_tenants (lease_id) where role = 'primary';
|
|
58
|
+
create index lease_tenants_tenant_idx on leasing.lease_tenants (tenant_id);
|
|
59
|
+
create index lease_tenants_organization_idx on leasing.lease_tenants (organization_id);
|
|
60
|
+
|
|
61
|
+
create table leasing.deposits (
|
|
62
|
+
id bigint generated by default as identity primary key,
|
|
63
|
+
organization_id integer not null references core.organizations (id),
|
|
64
|
+
lease_id bigint not null unique references leasing.leases (id),
|
|
65
|
+
amount numeric(10, 2) not null check (amount > 0),
|
|
66
|
+
received_on date not null,
|
|
67
|
+
status text not null default 'held' check (status in ('held', 'returned', 'partially_returned', 'withheld')),
|
|
68
|
+
returned_amount numeric(10, 2) not null default 0 check (returned_amount >= 0 and returned_amount <= amount),
|
|
69
|
+
returned_on date,
|
|
70
|
+
created_at timestamptz not null default now(),
|
|
71
|
+
updated_at timestamptz not null default now(),
|
|
72
|
+
check ((status = 'held') = (returned_on is null))
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
create index deposits_organization_idx on leasing.deposits (organization_id, created_at);
|
|
76
|
+
create index deposits_status_idx on leasing.deposits (organization_id, status);
|
|
77
|
+
|
|
78
|
+
create index leases_number_trgm_idx on leasing.leases using gin (number gin_trgm_ops);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
create schema billing;
|
|
2
|
+
|
|
3
|
+
-- One charge per lease and month, due on the first. paid_amount is the sum of its payments, kept by the triggers
|
|
4
|
+
-- below; the check on it means the payments of a charge never add up to more than the charge.
|
|
5
|
+
create table billing.rent_charges (
|
|
6
|
+
id bigint generated by default as identity primary key,
|
|
7
|
+
organization_id integer not null references core.organizations (id),
|
|
8
|
+
lease_id bigint not null references leasing.leases (id),
|
|
9
|
+
period date not null check (extract(day from period) = 1),
|
|
10
|
+
due_on date not null,
|
|
11
|
+
description text not null,
|
|
12
|
+
amount numeric(10, 2) not null check (amount > 0),
|
|
13
|
+
paid_amount numeric(10, 2) not null default 0 check (paid_amount >= 0 and paid_amount <= amount),
|
|
14
|
+
created_at timestamptz not null default now(),
|
|
15
|
+
updated_at timestamptz not null default now(),
|
|
16
|
+
unique (lease_id, period)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
create index rent_charges_organization_idx on billing.rent_charges (organization_id, period);
|
|
20
|
+
create index rent_charges_due_idx on billing.rent_charges (organization_id, due_on);
|
|
21
|
+
create index rent_charges_amount_idx on billing.rent_charges (organization_id, amount);
|
|
22
|
+
-- The open charges, a few percent of the table: what the arrears view reads.
|
|
23
|
+
create index rent_charges_open_idx on billing.rent_charges (organization_id, lease_id) where paid_amount < amount;
|
|
24
|
+
|
|
25
|
+
create table billing.payments (
|
|
26
|
+
id bigint generated by default as identity primary key,
|
|
27
|
+
organization_id integer not null references core.organizations (id),
|
|
28
|
+
charge_id bigint not null references billing.rent_charges (id),
|
|
29
|
+
paid_on date not null,
|
|
30
|
+
amount numeric(10, 2) not null check (amount > 0),
|
|
31
|
+
method text not null check (method in ('direct_debit', 'bank_transfer', 'ideal')),
|
|
32
|
+
reference text,
|
|
33
|
+
created_at timestamptz not null default now(),
|
|
34
|
+
updated_at timestamptz not null default now()
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
create index payments_charge_idx on billing.payments (charge_id);
|
|
38
|
+
create index payments_organization_idx on billing.payments (organization_id, paid_on);
|
|
39
|
+
create index payments_method_idx on billing.payments (organization_id, method);
|
|
40
|
+
|
|
41
|
+
create function billing.refresh_paid_amounts(ids bigint[]) returns void language sql as $$
|
|
42
|
+
update billing.rent_charges c
|
|
43
|
+
set paid_amount = s.paid, updated_at = now()
|
|
44
|
+
from (
|
|
45
|
+
select ch.id, coalesce((select sum(p.amount) from billing.payments p where p.charge_id = ch.id), 0) as paid
|
|
46
|
+
from billing.rent_charges ch
|
|
47
|
+
where ch.id = any (ids)
|
|
48
|
+
) s
|
|
49
|
+
where c.id = s.id and c.paid_amount is distinct from s.paid
|
|
50
|
+
$$;
|
|
51
|
+
|
|
52
|
+
create function billing.payments_inserted() returns trigger language plpgsql as $$
|
|
53
|
+
begin
|
|
54
|
+
perform billing.refresh_paid_amounts(array(select distinct charge_id from new_rows));
|
|
55
|
+
return null;
|
|
56
|
+
end $$;
|
|
57
|
+
|
|
58
|
+
create function billing.payments_updated() returns trigger language plpgsql as $$
|
|
59
|
+
begin
|
|
60
|
+
perform billing.refresh_paid_amounts(array(select charge_id from old_rows union select charge_id from new_rows));
|
|
61
|
+
return null;
|
|
62
|
+
end $$;
|
|
63
|
+
|
|
64
|
+
create function billing.payments_deleted() returns trigger language plpgsql as $$
|
|
65
|
+
begin
|
|
66
|
+
perform billing.refresh_paid_amounts(array(select distinct charge_id from old_rows));
|
|
67
|
+
return null;
|
|
68
|
+
end $$;
|
|
69
|
+
|
|
70
|
+
-- Transition tables cannot be combined with several events, so one trigger per event.
|
|
71
|
+
create trigger payments_inserted after insert on billing.payments
|
|
72
|
+
referencing new table as new_rows for each statement execute function billing.payments_inserted();
|
|
73
|
+
create trigger payments_updated after update on billing.payments
|
|
74
|
+
referencing old table as old_rows new table as new_rows for each statement execute function billing.payments_updated();
|
|
75
|
+
create trigger payments_deleted after delete on billing.payments
|
|
76
|
+
referencing old table as old_rows for each statement execute function billing.payments_deleted();
|
|
77
|
+
|
|
78
|
+
-- Outstanding balance per lease: every charge minus its payments, for leases that still owe something.
|
|
79
|
+
create view billing.arrears as
|
|
80
|
+
select
|
|
81
|
+
c.lease_id,
|
|
82
|
+
c.organization_id,
|
|
83
|
+
count(*)::integer as open_charges,
|
|
84
|
+
sum(c.amount - c.paid_amount) as balance,
|
|
85
|
+
min(c.due_on) as oldest_due_on
|
|
86
|
+
from billing.rent_charges c
|
|
87
|
+
where c.paid_amount < c.amount
|
|
88
|
+
group by c.lease_id, c.organization_id;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
create schema maintenance;
|
|
2
|
+
|
|
3
|
+
create type maintenance.ticket_status as enum ('reported', 'scheduled', 'done', 'cancelled');
|
|
4
|
+
|
|
5
|
+
create table maintenance.vendors (
|
|
6
|
+
id integer generated by default as identity primary key,
|
|
7
|
+
organization_id integer not null references core.organizations (id),
|
|
8
|
+
name text not null,
|
|
9
|
+
trade text not null check (trade in ('plumbing', 'heating', 'electrical', 'roofing', 'carpentry', 'locksmith', 'pest_control', 'general')),
|
|
10
|
+
email text,
|
|
11
|
+
phone text,
|
|
12
|
+
hourly_rate numeric(8, 2) not null check (hourly_rate > 0),
|
|
13
|
+
active boolean not null default true,
|
|
14
|
+
created_at timestamptz not null default now(),
|
|
15
|
+
updated_at timestamptz not null default now()
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
create index vendors_trade_idx on maintenance.vendors (organization_id, trade);
|
|
19
|
+
|
|
20
|
+
create table maintenance.tickets (
|
|
21
|
+
id bigint generated by default as identity primary key,
|
|
22
|
+
organization_id integer not null references core.organizations (id),
|
|
23
|
+
number text not null,
|
|
24
|
+
property_id bigint not null references portfolio.properties (id),
|
|
25
|
+
unit_id bigint references portfolio.units (id),
|
|
26
|
+
reported_by bigint references leasing.tenants (id),
|
|
27
|
+
assigned_to integer references core.users (id),
|
|
28
|
+
category text not null check (category in ('heating', 'plumbing', 'electrical', 'leak', 'mould', 'locks', 'appliances', 'windows_doors', 'garden', 'pests')),
|
|
29
|
+
priority text not null default 'normal' check (priority in ('low', 'normal', 'high', 'urgent')),
|
|
30
|
+
status maintenance.ticket_status not null default 'reported',
|
|
31
|
+
title text not null,
|
|
32
|
+
description text,
|
|
33
|
+
reported_at timestamptz not null default now(),
|
|
34
|
+
closed_at timestamptz,
|
|
35
|
+
created_at timestamptz not null default now(),
|
|
36
|
+
updated_at timestamptz not null default now(),
|
|
37
|
+
unique (organization_id, number),
|
|
38
|
+
check ((status in ('done', 'cancelled')) = (closed_at is not null))
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
create index tickets_organization_idx on maintenance.tickets (organization_id, created_at);
|
|
42
|
+
create index tickets_status_idx on maintenance.tickets (organization_id, status);
|
|
43
|
+
create index tickets_priority_idx on maintenance.tickets (organization_id, priority);
|
|
44
|
+
create index tickets_category_idx on maintenance.tickets (organization_id, category);
|
|
45
|
+
create index tickets_reported_idx on maintenance.tickets (organization_id, reported_at);
|
|
46
|
+
create index tickets_property_idx on maintenance.tickets (property_id);
|
|
47
|
+
create index tickets_unit_idx on maintenance.tickets (unit_id);
|
|
48
|
+
create index tickets_reported_by_idx on maintenance.tickets (reported_by);
|
|
49
|
+
create index tickets_assigned_idx on maintenance.tickets (assigned_to);
|
|
50
|
+
create index tickets_title_trgm_idx on maintenance.tickets using gin (title gin_trgm_ops);
|
|
51
|
+
|
|
52
|
+
-- A ticket moves forward only: reported -> scheduled -> done, and reported or scheduled -> cancelled.
|
|
53
|
+
-- Closing it stamps closed_at; the check on the table keeps the two in step.
|
|
54
|
+
create function maintenance.ticket_status_changed() returns trigger language plpgsql as $$
|
|
55
|
+
begin
|
|
56
|
+
if new.status = old.status then return new; end if;
|
|
57
|
+
if not ((old.status = 'reported' and new.status in ('scheduled', 'cancelled'))
|
|
58
|
+
or (old.status = 'scheduled' and new.status in ('done', 'cancelled'))) then
|
|
59
|
+
raise exception 'a ticket cannot go from % to %', old.status, new.status using errcode = 'check_violation';
|
|
60
|
+
end if;
|
|
61
|
+
if new.status in ('done', 'cancelled') then new.closed_at := coalesce(new.closed_at, now()); end if;
|
|
62
|
+
return new;
|
|
63
|
+
end $$;
|
|
64
|
+
|
|
65
|
+
create trigger ticket_status_changed before update of status on maintenance.tickets
|
|
66
|
+
for each row execute function maintenance.ticket_status_changed();
|
|
67
|
+
|
|
68
|
+
create table maintenance.work_orders (
|
|
69
|
+
id bigint generated by default as identity primary key,
|
|
70
|
+
organization_id integer not null references core.organizations (id),
|
|
71
|
+
ticket_id bigint not null references maintenance.tickets (id),
|
|
72
|
+
vendor_id integer not null references maintenance.vendors (id),
|
|
73
|
+
scheduled_for date not null,
|
|
74
|
+
completed_on date,
|
|
75
|
+
hours numeric(5, 2),
|
|
76
|
+
cost numeric(10, 2),
|
|
77
|
+
notes text,
|
|
78
|
+
created_at timestamptz not null default now(),
|
|
79
|
+
updated_at timestamptz not null default now(),
|
|
80
|
+
check (completed_on is null or completed_on >= scheduled_for)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
create index work_orders_ticket_idx on maintenance.work_orders (ticket_id);
|
|
84
|
+
create index work_orders_vendor_idx on maintenance.work_orders (vendor_id);
|
|
85
|
+
create index work_orders_scheduled_idx on maintenance.work_orders (organization_id, scheduled_for);
|
|
86
|
+
|
|
87
|
+
create table maintenance.inspections (
|
|
88
|
+
id bigint generated by default as identity primary key,
|
|
89
|
+
organization_id integer not null references core.organizations (id),
|
|
90
|
+
unit_id bigint not null references portfolio.units (id),
|
|
91
|
+
lease_id bigint references leasing.leases (id),
|
|
92
|
+
inspector_id integer not null references core.users (id),
|
|
93
|
+
kind text not null check (kind in ('move_in', 'move_out', 'periodic')),
|
|
94
|
+
inspected_on date not null,
|
|
95
|
+
condition text not null check (condition in ('good', 'fair', 'poor')),
|
|
96
|
+
notes text,
|
|
97
|
+
created_at timestamptz not null default now(),
|
|
98
|
+
updated_at timestamptz not null default now()
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
create index inspections_organization_idx on maintenance.inspections (organization_id, inspected_on);
|
|
102
|
+
create index inspections_unit_idx on maintenance.inspections (unit_id);
|
|
103
|
+
create index inspections_lease_idx on maintenance.inspections (lease_id);
|
|
104
|
+
create index inspections_inspector_idx on maintenance.inspections (inspector_id);
|
|
105
|
+
create index inspections_kind_idx on maintenance.inspections (organization_id, kind);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- What is still owed on a charge, as a column so it can be filtered, sorted and indexed. Postgres computes it on every
|
|
2
|
+
-- write, so it follows paid_amount whether the payment triggers or the seed loader (triggers off) sets it.
|
|
3
|
+
alter table billing.rent_charges add column outstanding numeric(10, 2) generated always as (amount - paid_amount) stored;
|
|
4
|
+
|
|
5
|
+
create or replace view billing.arrears as
|
|
6
|
+
select
|
|
7
|
+
c.lease_id,
|
|
8
|
+
c.organization_id,
|
|
9
|
+
count(*)::integer as open_charges,
|
|
10
|
+
sum(c.outstanding) as balance,
|
|
11
|
+
min(c.due_on) as oldest_due_on
|
|
12
|
+
from billing.rent_charges c
|
|
13
|
+
where c.paid_amount < c.amount
|
|
14
|
+
group by c.lease_id, c.organization_id;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- The auth schema changes this version of Protobase needs, written by `protobase auth migration`.
|
|
2
|
+
-- Applied with the project's other migrations; edit or remove statements the project handles differently.
|
|
3
|
+
|
|
4
|
+
create schema if not exists "auth";
|
|
5
|
+
|
|
6
|
+
create table if not exists "auth"."user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "email_verified" boolean not null, "image" text, "created_at" timestamptz default CURRENT_TIMESTAMP not null, "updated_at" timestamptz default CURRENT_TIMESTAMP not null, "role" text, "banned" boolean, "ban_reason" text, "ban_expires" timestamptz, "two_factor_enabled" boolean);
|
|
7
|
+
|
|
8
|
+
create table if not exists "auth"."session" ("id" text not null primary key, "expires_at" timestamptz not null, "token" text not null unique, "created_at" timestamptz default CURRENT_TIMESTAMP not null, "updated_at" timestamptz not null, "ip_address" text, "user_agent" text, "user_id" text not null references "auth"."user" ("id") on delete cascade, "impersonated_by" text);
|
|
9
|
+
|
|
10
|
+
create table if not exists "auth"."account" ("id" text not null primary key, "account_id" text not null, "provider_id" text not null, "user_id" text not null references "auth"."user" ("id") on delete cascade, "access_token" text, "refresh_token" text, "id_token" text, "access_token_expires_at" timestamptz, "refresh_token_expires_at" timestamptz, "scope" text, "password" text, "created_at" timestamptz default CURRENT_TIMESTAMP not null, "updated_at" timestamptz not null);
|
|
11
|
+
|
|
12
|
+
create table if not exists "auth"."verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expires_at" timestamptz not null, "created_at" timestamptz default CURRENT_TIMESTAMP not null, "updated_at" timestamptz default CURRENT_TIMESTAMP not null);
|
|
13
|
+
|
|
14
|
+
create table if not exists "auth"."jwks" ("id" text not null primary key, "public_key" text not null, "private_key" text not null, "created_at" timestamptz not null, "expires_at" timestamptz, "alg" text, "crv" text);
|
|
15
|
+
|
|
16
|
+
create table if not exists "auth"."two_factor" ("id" text not null primary key, "secret" text not null, "backup_codes" text not null, "user_id" text not null references "auth"."user" ("id") on delete cascade, "verified" boolean, "failed_verification_count" integer, "locked_until" timestamptz);
|
|
17
|
+
|
|
18
|
+
create table if not exists "auth"."passkey" ("id" text not null primary key, "name" text, "public_key" text not null, "user_id" text not null references "auth"."user" ("id") on delete cascade, "credential_id" text not null, "counter" integer not null, "device_type" text not null, "backed_up" boolean not null, "transports" text, "created_at" timestamptz, "aaguid" text);
|
|
19
|
+
|
|
20
|
+
create table if not exists "auth"."sign_in_policy" ("id" text not null primary key, "password" text not null, "email_code" text not null, "passkey" text not null, "two_factor" text not null, "staff_access" text not null, "platform_sign_in" text not null, "created_at" timestamptz not null, "created_by" text);
|
|
21
|
+
|
|
22
|
+
create table if not exists "auth"."staff_sign_in" ("id" text not null primary key, "user_id" text not null, "user_email" text not null, "staff" text not null, "staff_name" text, "staff_subject" text not null, "issuer" text not null, "reason" text not null, "session_id" text not null, "started_at" timestamptz not null, "expires_at" timestamptz not null, "ended_at" timestamptz);
|
|
23
|
+
|
|
24
|
+
create index if not exists "session_user_id_idx" on "auth"."session" ("user_id");
|
|
25
|
+
|
|
26
|
+
create index if not exists "account_user_id_idx" on "auth"."account" ("user_id");
|
|
27
|
+
|
|
28
|
+
create index if not exists "verification_identifier_idx" on "auth"."verification" ("identifier");
|
|
29
|
+
|
|
30
|
+
create index if not exists "two_factor_secret_idx" on "auth"."two_factor" ("secret");
|
|
31
|
+
|
|
32
|
+
create index if not exists "two_factor_user_id_idx" on "auth"."two_factor" ("user_id");
|
|
33
|
+
|
|
34
|
+
create index if not exists "passkey_user_id_idx" on "auth"."passkey" ("user_id");
|
|
35
|
+
|
|
36
|
+
create index if not exists "passkey_credential_id_idx" on "auth"."passkey" ("credential_id");
|