@@ -22,30 +22,21 @@ alter table public.companies
2222alter table public .companies
2323 alter column default_hours_per_day set default 8 ;
2424
25+ -- Only fill null hours; do not rewrite intentional fee/max/license/vacation values.
2526update public .companies
26- set
27- company_fee_pct = case when company_fee_pct = 0 then 5 else company_fee_pct end,
28- rem_max_percent = case when rem_max_percent = 100 then 75 else rem_max_percent end,
29- default_license_pct = case when default_license_pct = 0 then 30 else default_license_pct end,
30- default_hours_per_day = coalesce(default_hours_per_day, 8 ),
31- max_vacation_days_per_year = coalesce(max_vacation_days_per_year, 15 )
32- where deleted = false;
27+ set default_hours_per_day = 8
28+ where deleted = false and default_hours_per_day is null ;
3329
3430alter table public .projects
3531 alter column hours_per_day set default 8 ;
3632
37- update public .projects
38- set hours_per_day = 8
39- where hours_per_day = 7 and deleted = false;
40-
4133alter table public .company_members
4234 add column if not exists rem_license_recipient boolean not null default false;
4335
4436comment on column public.company_members.rem_license_recipient is
4537 ' Receives product-service / standalone license splits (exactly two active recipients required).' ;
4638
47- -- Seed: if a company has rem partners but no license recipients, promote the two
48- -- oldest rem_partner members (or all rem_partners when fewer than two).
39+ -- Seed license recipients only when a company has at least two rem partners and none set yet.
4940with ranked as (
5041 select
5142 id,
@@ -59,7 +50,8 @@ update public.company_members cm
5950set rem_license_recipient = true
6051from ranked r
6152where cm .id = r .id
62- and r .rn <= least(2 , r .n )
53+ and r .n >= 2
54+ and r .rn <= 2
6355 and not exists (
6456 select 1 from public .company_members x
6557 where x .company_id = r .company_id
@@ -203,6 +195,39 @@ as $$
203195 );
204196$$;
205197
198+ revoke all on function public .company_hours_per_day (uuid) from public;
199+ revoke all on function public .company_hours_per_day (uuid) from anon, authenticated;
200+
201+ create or replace function public ._chrono_restore_jungle_period(
202+ p_company_id uuid,
203+ p_period date
204+ )
205+ returns void
206+ language plpgsql
207+ security definer
208+ set search_path = ' '
209+ as $$
210+ begin
211+ update public .jungle_tjm_queue_entries e
212+ set remaining_cents = remaining_cents + s .amount_cents , updated_at = now()
213+ from public .jungle_tjm_queue_settlements s
214+ where s .queue_entry_id = e .id
215+ and s .company_id = p_company_id
216+ and s .period_month = p_period
217+ and e .deleted = false;
218+
219+ delete from public .jungle_tjm_queue_settlements
220+ where company_id = p_company_id and period_month = p_period;
221+
222+ update public .jungle_tjm_queue_entries
223+ set deleted = true, updated_at = now()
224+ where company_id = p_company_id and period_month = p_period and deleted = false;
225+ end;
226+ $$;
227+
228+ revoke all on function public ._chrono_restore_jungle_period (uuid, date ) from public;
229+ revoke all on function public ._chrono_restore_jungle_period (uuid, date ) from anon, authenticated;
230+
206231create or replace function public .enforce_vacation_allowance()
207232returns trigger
208233language plpgsql
@@ -215,6 +240,11 @@ declare
215240 v_year integer ;
216241 v_used numeric ;
217242 v_adding numeric ;
243+ v_period date ;
244+ v_cap_minutes numeric ;
245+ v_work_minutes numeric ;
246+ v_leave_minutes numeric ;
247+ v_new_minutes numeric ;
218248begin
219249 if tg_op = ' DELETE' then
220250 return old;
@@ -225,31 +255,68 @@ begin
225255
226256 select max_vacation_days_per_year into v_max
227257 from public .companies where id = new .company_id ;
228- v_max := coalesce(v_max, 15 );
258+ -- null = unlimited (documented company policy)
259+ if v_max is not null then
260+ v_hpd := public .company_hours_per_day (new .company_id );
261+ v_year := extract(year from new .off_date )::integer ;
262+
263+ select coalesce(sum (
264+ case
265+ when t .duration_minutes is null then 1
266+ else t .duration_minutes ::numeric / nullif(v_hpd * 60 , 0 )
267+ end
268+ ), 0 ) into v_used
269+ from public .time_off t
270+ where t .company_id = new .company_id
271+ and t .user_id = new .user_id
272+ and t .kind = ' vacation'
273+ and extract(year from t .off_date ) = v_year
274+ and t .id is distinct from new .id ;
275+
276+ v_adding := case
277+ when new .duration_minutes is null then 1
278+ else new .duration_minutes ::numeric / nullif(v_hpd * 60 , 0 )
279+ end;
280+
281+ if v_used + v_adding > v_max + 1e- 9 then
282+ raise exception ' Vacation allowance exceeded (% days/year)' , v_max;
283+ end if;
284+ end if;
285+
286+ -- Monthly capacity: work + leave ≤ 22 × company hours/day
229287 v_hpd := public .company_hours_per_day (new .company_id );
230- v_year := extract(year from new .off_date )::integer ;
288+ v_cap_minutes := 22 * v_hpd * 60 ;
289+ v_period := date_trunc(' month' , new .off_date )::date ;
290+ v_new_minutes := case
291+ when new .duration_minutes is null then v_hpd * 60
292+ else greatest(0 , new .duration_minutes )
293+ end;
294+
295+ select coalesce(sum (greatest(0 , te .duration_minutes )), 0 ) into v_work_minutes
296+ from public .time_entries te
297+ where te .company_id = new .company_id
298+ and te .user_id = new .user_id
299+ and te .deleted = false
300+ and te .status is distinct from ' rejected'
301+ and date_trunc(' month' , te .entry_date )::date = v_period;
231302
232303 select coalesce(sum (
233304 case
234- when t .duration_minutes is null then 1
235- else t . duration_minutes :: numeric / nullif(v_hpd * 60 , 0 )
305+ when t .duration_minutes is null then v_hpd * 60
306+ else greatest( 0 , t . duration_minutes )
236307 end
237- ), 0 ) into v_used
308+ ), 0 ) into v_leave_minutes
238309 from public .time_off t
239310 where t .company_id = new .company_id
240311 and t .user_id = new .user_id
241312 and t .kind = ' vacation'
242- and extract(year from t .off_date ) = v_year
313+ and date_trunc( ' month ' , t .off_date ):: date = v_period
243314 and t .id is distinct from new .id ;
244315
245- v_adding := case
246- when new .duration_minutes is null then 1
247- else new .duration_minutes ::numeric / nullif(v_hpd * 60 , 0 )
248- end;
249-
250- if v_used + v_adding > v_max + 1e- 9 then
251- raise exception ' Vacation allowance exceeded (% days/year)' , v_max;
316+ if v_work_minutes + v_leave_minutes + v_new_minutes > v_cap_minutes + 1e- 9 then
317+ raise exception ' Monthly capacity exceeded (22 × %h)' , v_hpd;
252318 end if;
319+
253320 return new;
254321end;
255322$$;
@@ -280,10 +347,7 @@ begin
280347 return new;
281348 end if;
282349
283- v_hpd := coalesce(
284- (select hours_per_day from public .projects where id = new .project_id ),
285- public .company_hours_per_day (new .company_id )
286- );
350+ v_hpd := public .company_hours_per_day (new .company_id );
287351 v_cap_minutes := 22 * v_hpd * 60 ;
288352 v_period := date_trunc(' month' , new .entry_date )::date ;
289353 v_new_minutes := greatest(0 , coalesce(new .duration_minutes , 0 ));
@@ -860,15 +924,8 @@ begin
860924 end loop;
861925 end loop;
862926
863- -- ---- jungle: enqueue period, fee on paid revenue, FIFO dequeue post-fee ----
864- delete from public .jungle_tjm_queue_settlements s
865- using public .jungle_tjm_queue_entries e
866- where s .queue_entry_id = e .id
867- and e .company_id = p_company_id
868- and e .period_month = v_period;
869- update public .jungle_tjm_queue_entries
870- set deleted = true, updated_at = now()
871- where company_id = p_company_id and period_month = v_period and deleted = false;
927+ -- ---- jungle: restore prior settlements for this period, then re-enqueue ----
928+ perform public ._chrono_restore_jungle_period (p_company_id, v_period);
872929
873930 for v_proj in
874931 select id, jungle_fictitious_tjm_cents, hours_per_day, company_id
@@ -1017,9 +1074,10 @@ begin
10171074 and user_id is not null
10181075 and bucket not in (' company_fee' , ' staffing_tjm' )
10191076 and (
1020- project_id = p_project_id
1077+ ( project_id = p_project_id)
10211078 or (
10221079 project_id is null
1080+ and bucket = ' product_pool'
10231081 and v_policy = ' product_pool'
10241082 and p_project_id = v_pool_project
10251083 )
0 commit comments