Removed tax settings
This commit is contained in:
@@ -31,7 +31,7 @@ public class BudgetsController(AppDbContext db, BudgetAuthorizationService authz
|
|||||||
var budgets = await db.Budgets
|
var budgets = await db.Budgets
|
||||||
.Where(b => b.OwnerUserId == userId ||
|
.Where(b => b.OwnerUserId == userId ||
|
||||||
b.Shares.Any(s => s.SharedWithUserId == userId && !s.IsPending))
|
b.Shares.Any(s => s.SharedWithUserId == userId && !s.IsPending))
|
||||||
.Select(b => new BudgetDto(b.Id, b.Name, b.EffectiveTaxRate, b.CreatedAt, b.UpdatedAt))
|
.Select(b => new BudgetDto(b.Id, b.Name, b.CreatedAt, b.UpdatedAt))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return Ok(budgets);
|
return Ok(budgets);
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ public class BudgetsController(AppDbContext db, BudgetAuthorizationService authz
|
|||||||
db.Budgets.Add(budget);
|
db.Budgets.Add(budget);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return CreatedAtAction(nameof(Get), new { id = budget.Id },
|
return CreatedAtAction(nameof(Get), new { id = budget.Id },
|
||||||
new BudgetDto(budget.Id, budget.Name, budget.EffectiveTaxRate, budget.CreatedAt, budget.UpdatedAt));
|
new BudgetDto(budget.Id, budget.Name, budget.CreatedAt, budget.UpdatedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
@@ -62,7 +62,7 @@ public class BudgetsController(AppDbContext db, BudgetAuthorizationService authz
|
|||||||
if (!await authz.CanReadAsync(id, userId)) return Forbid();
|
if (!await authz.CanReadAsync(id, userId)) return Forbid();
|
||||||
var b = await db.Budgets.FindAsync(id);
|
var b = await db.Budgets.FindAsync(id);
|
||||||
if (b is null) return NotFound();
|
if (b is null) return NotFound();
|
||||||
return Ok(new BudgetDto(b.Id, b.Name, b.EffectiveTaxRate, b.CreatedAt, b.UpdatedAt));
|
return Ok(new BudgetDto(b.Id, b.Name, b.CreatedAt, b.UpdatedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
@@ -83,7 +83,7 @@ public class BudgetsController(AppDbContext db, BudgetAuthorizationService authz
|
|||||||
{
|
{
|
||||||
return Conflict(new { error = "The budget was modified by another user. Please refresh and retry." });
|
return Conflict(new { error = "The budget was modified by another user. Please refresh and retry." });
|
||||||
}
|
}
|
||||||
return Ok(new BudgetDto(b.Id, b.Name, b.EffectiveTaxRate, b.CreatedAt, b.UpdatedAt));
|
return Ok(new BudgetDto(b.Id, b.Name, b.CreatedAt, b.UpdatedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using Budget.Infrastructure.Data;
|
|||||||
using Budget.Infrastructure.Services;
|
using Budget.Infrastructure.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Budget.Api.Controllers;
|
namespace Budget.Api.Controllers;
|
||||||
@@ -34,10 +33,10 @@ public class SummaryController(AppDbContext db, BudgetAuthorizationService authz
|
|||||||
if (budget is null) return NotFound();
|
if (budget is null) return NotFound();
|
||||||
|
|
||||||
var incomes = await db.Incomes.Where(i => i.BudgetId == budgetId).ToListAsync();
|
var incomes = await db.Incomes.Where(i => i.BudgetId == budgetId).ToListAsync();
|
||||||
var outgos = await db.Outgos.Where(o => o.BudgetId == budgetId).ToListAsync();
|
var outgos = await db.Outgos.Where(o => o.BudgetId == budgetId).ToListAsync();
|
||||||
|
|
||||||
var monthlyIncome = incomes.Sum(i => FrequencyCalculator.ToMonthly(i.Amount, i.Frequency));
|
var monthlyIncome = incomes.Sum(i => FrequencyCalculator.ToMonthly(i.Amount, i.Frequency));
|
||||||
var annualIncome = monthlyIncome * 12m;
|
var annualIncome = monthlyIncome * 12m;
|
||||||
|
|
||||||
var typeTargets = new Dictionary<OutgoType, int>
|
var typeTargets = new Dictionary<OutgoType, int>
|
||||||
{
|
{
|
||||||
@@ -51,43 +50,20 @@ public class SummaryController(AppDbContext db, BudgetAuthorizationService authz
|
|||||||
|
|
||||||
foreach (var (type, target) in typeTargets)
|
foreach (var (type, target) in typeTargets)
|
||||||
{
|
{
|
||||||
var typeOutgos = outgos.Where(o => o.Type == type).ToList();
|
var typeOutgos = outgos.Where(o => o.Type == type).ToList();
|
||||||
var monthlyTotal = typeOutgos.Sum(o => FrequencyCalculator.ToMonthly(o.Amount, o.Frequency));
|
var monthlyTotal = typeOutgos.Sum(o => FrequencyCalculator.ToMonthly(o.Amount, o.Frequency));
|
||||||
var annualTotal = monthlyTotal * 12m;
|
var annualTotal = monthlyTotal * 12m;
|
||||||
totalMonthlyOutgo += monthlyTotal;
|
totalMonthlyOutgo += monthlyTotal;
|
||||||
var percent = monthlyIncome > 0 ? Math.Round(monthlyTotal / monthlyIncome * 100, 2) : 0m;
|
var percent = monthlyIncome > 0 ? Math.Round(monthlyTotal / monthlyIncome * 100, 2) : 0m;
|
||||||
var maxAmount = monthlyIncome * target / 100m;
|
var maxAmount = monthlyIncome * target / 100m;
|
||||||
var remaining = maxAmount - monthlyTotal;
|
var remaining = maxAmount - monthlyTotal;
|
||||||
breakdown.Add(new SummaryBreakdownItem(type.ToString(), target, monthlyTotal, annualTotal, percent, maxAmount, remaining));
|
breakdown.Add(new SummaryBreakdownItem(type.ToString(), target, monthlyTotal, annualTotal, percent, maxAmount, remaining));
|
||||||
}
|
}
|
||||||
|
|
||||||
var unspent = monthlyIncome - totalMonthlyOutgo;
|
var unspent = monthlyIncome - totalMonthlyOutgo;
|
||||||
var unspentPercent = monthlyIncome > 0 ? Math.Round(unspent / monthlyIncome * 100, 2) : 0m;
|
var unspentPercent = monthlyIncome > 0 ? Math.Round(unspent / monthlyIncome * 100, 2) : 0m;
|
||||||
breakdown.Add(new SummaryBreakdownItem("Unspent", null, unspent, unspent * 12m, unspentPercent, null, null));
|
breakdown.Add(new SummaryBreakdownItem("Unspent", null, unspent, unspent * 12m, unspentPercent, null, null));
|
||||||
|
|
||||||
var annualOutgoTotal = totalMonthlyOutgo * 12m;
|
return Ok(new SummaryDto(monthlyIncome, annualIncome, breakdown));
|
||||||
var minimumAnnualGross = budget.EffectiveTaxRate < 1m
|
|
||||||
? annualOutgoTotal / (1m - budget.EffectiveTaxRate)
|
|
||||||
: 0m;
|
|
||||||
|
|
||||||
return Ok(new SummaryDto(
|
|
||||||
monthlyIncome,
|
|
||||||
annualIncome,
|
|
||||||
breakdown,
|
|
||||||
new PreTaxIncomeDto(budget.EffectiveTaxRate, minimumAnnualGross, minimumAnnualGross / 12m)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("tax-rate")]
|
|
||||||
[EnableRateLimiting("writes")]
|
|
||||||
public async Task<IActionResult> UpdateTaxRate(Guid budgetId, [FromBody] UpdateTaxRateRequest req)
|
|
||||||
{
|
|
||||||
if (TryGetUserId(out var userId) is { } err) return err;
|
|
||||||
if (!await authz.CanWriteAsync(budgetId, userId)) return Forbid();
|
|
||||||
var budget = await db.Budgets.FindAsync(budgetId);
|
|
||||||
if (budget is null) return NotFound();
|
|
||||||
budget.EffectiveTaxRate = req.EffectiveTaxRate;
|
|
||||||
budget.UpdatedAt = DateTimeOffset.UtcNow;
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { api } from './client';
|
import { api } from './client';
|
||||||
import type { SummaryDto } from '../types/index';
|
import type { SummaryDto } from '../types/index';
|
||||||
|
|
||||||
@@ -8,16 +8,3 @@ export function useSummary(budgetId: string) {
|
|||||||
queryFn: () => api.get<SummaryDto>(`/api/budgets/${budgetId}/summary`),
|
queryFn: () => api.get<SummaryDto>(`/api/budgets/${budgetId}/summary`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateTaxRate(budgetId: string) {
|
|
||||||
const qc = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (effectiveTaxRate: number) =>
|
|
||||||
api.put<void>(`/api/budgets/${budgetId}/summary/tax-rate`, { effectiveTaxRate }),
|
|
||||||
onSuccess: () => {
|
|
||||||
qc.invalidateQueries({ queryKey: ['budgets', budgetId, 'summary'] });
|
|
||||||
qc.invalidateQueries({ queryKey: ['budgets', budgetId] });
|
|
||||||
},
|
|
||||||
meta: { successMessage: 'Tax rate saved', errorMessage: 'Failed to save tax rate' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,14 +6,11 @@ import type { SharePermission } from '../types/index';
|
|||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
import { BudgetNav } from '../components/BudgetNav';
|
import { BudgetNav } from '../components/BudgetNav';
|
||||||
import { useBudget, useUpdateBudget } from '../api/budgets';
|
import { useBudget, useUpdateBudget } from '../api/budgets';
|
||||||
import { useUpdateTaxRate } from '../api/summary';
|
|
||||||
import { useShares, useAddShare, useUpdateShare, useRevokeShare } from '../api/shares';
|
import { useShares, useAddShare, useUpdateShare, useRevokeShare } from '../api/shares';
|
||||||
import {
|
import {
|
||||||
createBudgetSchema,
|
createBudgetSchema,
|
||||||
updateTaxRateSchema,
|
|
||||||
createShareSchema,
|
createShareSchema,
|
||||||
type CreateBudgetInput,
|
type CreateBudgetInput,
|
||||||
type UpdateTaxRateInput,
|
|
||||||
type CreateShareInput,
|
type CreateShareInput,
|
||||||
} from '../schemas/index';
|
} from '../schemas/index';
|
||||||
|
|
||||||
@@ -23,8 +20,7 @@ export function SettingsPage() {
|
|||||||
const { data: shares = [] } = useShares(budgetId!);
|
const { data: shares = [] } = useShares(budgetId!);
|
||||||
|
|
||||||
const updateBudget = useUpdateBudget(budgetId!);
|
const updateBudget = useUpdateBudget(budgetId!);
|
||||||
const updateTaxRate = useUpdateTaxRate(budgetId!);
|
const addShare = useAddShare(budgetId!);
|
||||||
const addShare = useAddShare(budgetId!);
|
|
||||||
const updateShare = useUpdateShare(budgetId!);
|
const updateShare = useUpdateShare(budgetId!);
|
||||||
const revokeShare = useRevokeShare(budgetId!);
|
const revokeShare = useRevokeShare(budgetId!);
|
||||||
|
|
||||||
@@ -33,31 +29,19 @@ export function SettingsPage() {
|
|||||||
defaultValues: { name: '' },
|
defaultValues: { name: '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const taxForm = useForm<UpdateTaxRateInput>({
|
|
||||||
resolver: zodResolver(updateTaxRateSchema),
|
|
||||||
defaultValues: { effectiveTaxRate: 0 },
|
|
||||||
});
|
|
||||||
|
|
||||||
const shareForm = useForm<CreateShareInput>({
|
const shareForm = useForm<CreateShareInput>({
|
||||||
resolver: zodResolver(createShareSchema),
|
resolver: zodResolver(createShareSchema),
|
||||||
defaultValues: { email: '', permission: 'View' },
|
defaultValues: { email: '', permission: 'View' },
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (budget) {
|
if (budget) renameForm.reset({ name: budget.name });
|
||||||
renameForm.reset({ name: budget.name });
|
|
||||||
taxForm.reset({ effectiveTaxRate: Math.round(budget.effectiveTaxRate * 100) });
|
|
||||||
}
|
|
||||||
}, [budget]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [budget]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const onRename = async (data: CreateBudgetInput) => {
|
const onRename = async (data: CreateBudgetInput) => {
|
||||||
await updateBudget.mutateAsync({ name: data.name });
|
await updateBudget.mutateAsync({ name: data.name });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSaveTaxRate = async (data: UpdateTaxRateInput) => {
|
|
||||||
await updateTaxRate.mutateAsync(data.effectiveTaxRate / 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddShare = async (data: CreateShareInput) => {
|
const onAddShare = async (data: CreateShareInput) => {
|
||||||
await addShare.mutateAsync({ email: data.email, permission: data.permission });
|
await addShare.mutateAsync({ email: data.email, permission: data.permission });
|
||||||
shareForm.reset({ email: '', permission: 'View' });
|
shareForm.reset({ email: '', permission: 'View' });
|
||||||
@@ -90,33 +74,6 @@ export function SettingsPage() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card" style={{ maxWidth: '480px' }}>
|
|
||||||
<div className="section-title">Effective Tax Rate</div>
|
|
||||||
<form onSubmit={taxForm.handleSubmit(onSaveTaxRate)}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
|
||||||
<label htmlFor="tax-rate-settings">Rate (%)</label>
|
|
||||||
<input
|
|
||||||
id="tax-rate-settings"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="99"
|
|
||||||
style={{ width: '72px' }}
|
|
||||||
{...taxForm.register('effectiveTaxRate', { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-primary btn-sm"
|
|
||||||
disabled={taxForm.formState.isSubmitting || updateTaxRate.isPending}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{taxForm.formState.errors.effectiveTaxRate && (
|
|
||||||
<span className="field-error">{taxForm.formState.errors.effectiveTaxRate.message}</span>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="section-title">Sharing</div>
|
<div className="section-title">Sharing</div>
|
||||||
|
|
||||||
@@ -150,7 +107,9 @@ export function SettingsPage() {
|
|||||||
: <span className="badge badge-save">Active</span>}
|
: <span className="badge badge-save">Active</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="col-actions">
|
<td className="col-actions">
|
||||||
<button className="btn btn-danger btn-icon" title="Revoke access" onClick={() => revokeShare.mutate(s.id)}><Trash2 size={15} /></button>
|
<button className="btn btn-danger btn-icon" title="Revoke access" onClick={() => revokeShare.mutate(s.id)}>
|
||||||
|
<Trash2 size={15} />
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -38,13 +38,7 @@ export const createShareSchema = z.object({
|
|||||||
permission: z.enum(['View', 'Edit']),
|
permission: z.enum(['View', 'Edit']),
|
||||||
});
|
});
|
||||||
|
|
||||||
// UI shows percent (0-99); callers divide by 100 before sending to API
|
|
||||||
export const updateTaxRateSchema = z.object({
|
|
||||||
effectiveTaxRate: z.number().min(0, 'Must be ≥ 0').max(99, 'Must be less than 100'),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CreateBudgetInput = z.infer<typeof createBudgetSchema>;
|
export type CreateBudgetInput = z.infer<typeof createBudgetSchema>;
|
||||||
export type CreateIncomeInput = z.infer<typeof createIncomeSchema>;
|
export type CreateIncomeInput = z.infer<typeof createIncomeSchema>;
|
||||||
export type CreateOutgoInput = z.infer<typeof createOutgoSchema>;
|
export type CreateOutgoInput = z.infer<typeof createOutgoSchema>;
|
||||||
export type CreateShareInput = z.infer<typeof createShareSchema>;
|
export type CreateShareInput = z.infer<typeof createShareSchema>;
|
||||||
export type UpdateTaxRateInput = z.infer<typeof updateTaxRateSchema>;
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export interface OutgoDto {
|
|||||||
export interface BudgetDto {
|
export interface BudgetDto {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
effectiveTaxRate: number;
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -70,9 +69,4 @@ export interface SummaryDto {
|
|||||||
monthlyIncome: number;
|
monthlyIncome: number;
|
||||||
annualIncome: number;
|
annualIncome: number;
|
||||||
breakdown: SummaryBreakdownItem[];
|
breakdown: SummaryBreakdownItem[];
|
||||||
preTaxIncome: {
|
|
||||||
effectiveTaxRate: number;
|
|
||||||
minimumAnnualGross: number;
|
|
||||||
minimumMonthlyGross: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
|
|
||||||
namespace Budget.Core.DTOs;
|
namespace Budget.Core.DTOs;
|
||||||
|
|
||||||
public record BudgetDto(Guid Id, string Name, decimal EffectiveTaxRate, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);
|
public record BudgetDto(Guid Id, string Name, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);
|
||||||
|
|
||||||
public record CreateBudgetRequest([Required][MaxLength(200)] string Name);
|
public record CreateBudgetRequest([Required][MaxLength(200)] string Name);
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,7 @@ public record SummaryBreakdownItem(
|
|||||||
decimal? MaxAmount,
|
decimal? MaxAmount,
|
||||||
decimal? Remaining);
|
decimal? Remaining);
|
||||||
|
|
||||||
public record PreTaxIncomeDto(decimal EffectiveTaxRate, decimal MinimumAnnualGross, decimal MinimumMonthlyGross);
|
|
||||||
|
|
||||||
public record SummaryDto(
|
public record SummaryDto(
|
||||||
decimal MonthlyIncome,
|
decimal MonthlyIncome,
|
||||||
decimal AnnualIncome,
|
decimal AnnualIncome,
|
||||||
List<SummaryBreakdownItem> Breakdown,
|
List<SummaryBreakdownItem> Breakdown);
|
||||||
PreTaxIncomeDto PreTaxIncome);
|
|
||||||
|
|
||||||
public record UpdateTaxRateRequest(
|
|
||||||
[System.ComponentModel.DataAnnotations.Range(0.0, 0.9999, ErrorMessage = "Effective tax rate must be between 0 and 0.9999.")]
|
|
||||||
decimal EffectiveTaxRate);
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ public class Budget : ISoftDeletable
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
public required string OwnerUserId { get; set; }
|
public required string OwnerUserId { get; set; }
|
||||||
public decimal EffectiveTaxRate { get; set; } = 0.25m;
|
|
||||||
public DateTimeOffset CreatedAt { get; set; }
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
public DateTimeOffset UpdatedAt { get; set; }
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
|||||||
b.HasKey(x => x.Id);
|
b.HasKey(x => x.Id);
|
||||||
b.Property(x => x.Name).IsRequired().HasMaxLength(200);
|
b.Property(x => x.Name).IsRequired().HasMaxLength(200);
|
||||||
b.Property(x => x.OwnerUserId).IsRequired().HasMaxLength(200);
|
b.Property(x => x.OwnerUserId).IsRequired().HasMaxLength(200);
|
||||||
b.Property(x => x.EffectiveTaxRate).HasPrecision(5, 4);
|
|
||||||
b.HasMany(x => x.Incomes).WithOne(i => i.Budget).HasForeignKey(i => i.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
b.HasMany(x => x.Incomes).WithOne(i => i.Budget).HasForeignKey(i => i.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
||||||
b.HasMany(x => x.Outgos).WithOne(o => o.Budget).HasForeignKey(o => o.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
b.HasMany(x => x.Outgos).WithOne(o => o.Budget).HasForeignKey(o => o.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
||||||
b.HasMany(x => x.Shares).WithOne(s => s.Budget).HasForeignKey(s => s.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
b.HasMany(x => x.Shares).WithOne(s => s.Budget).HasForeignKey(s => s.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|||||||
Generated
+265
@@ -0,0 +1,265 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Budget.Infrastructure.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Budget.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260503121617_RemoveEffectiveTaxRate")]
|
||||||
|
partial class RemoveEffectiveTaxRate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Budget.Core.Models.Budget", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("OwnerUserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<uint>("xmin")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Budgets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Budget.Core.Models.BudgetShare", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("BudgetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPending")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("Permission")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("SharedWithEmail")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("SharedWithUserId")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BudgetId", "SharedWithEmail")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("BudgetShares");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Budget.Core.Models.Income", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)");
|
||||||
|
|
||||||
|
b.Property<Guid>("BudgetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Frequency")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BudgetId");
|
||||||
|
|
||||||
|
b.ToTable("Incomes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Budget.Core.Models.KnownUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("LastSeenAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("KnownUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Budget.Core.Models.Outgo", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)");
|
||||||
|
|
||||||
|
b.Property<Guid>("BudgetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Frequency")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("character varying(1000)");
|
||||||
|
|
||||||
|
b.Property<string>("PaymentSource")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BudgetId");
|
||||||
|
|
||||||
|
b.ToTable("Outgos");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Budget.Core.Models.BudgetShare", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Budget.Core.Models.Budget", "Budget")
|
||||||
|
.WithMany("Shares")
|
||||||
|
.HasForeignKey("BudgetId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Budget");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Budget.Core.Models.Income", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Budget.Core.Models.Budget", "Budget")
|
||||||
|
.WithMany("Incomes")
|
||||||
|
.HasForeignKey("BudgetId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Budget");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Budget.Core.Models.Outgo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Budget.Core.Models.Budget", "Budget")
|
||||||
|
.WithMany("Outgos")
|
||||||
|
.HasForeignKey("BudgetId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Budget");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Budget.Core.Models.Budget", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Incomes");
|
||||||
|
|
||||||
|
b.Navigation("Outgos");
|
||||||
|
|
||||||
|
b.Navigation("Shares");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Budget.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemoveEffectiveTaxRate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EffectiveTaxRate",
|
||||||
|
table: "Budgets");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "EffectiveTaxRate",
|
||||||
|
table: "Budgets",
|
||||||
|
type: "numeric(5,4)",
|
||||||
|
precision: 5,
|
||||||
|
scale: 4,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,10 +34,6 @@ namespace Budget.Infrastructure.Data.Migrations
|
|||||||
b.Property<DateTimeOffset?>("DeletedAt")
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<decimal>("EffectiveTaxRate")
|
|
||||||
.HasPrecision(5, 4)
|
|
||||||
.HasColumnType("numeric(5,4)");
|
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
b.Property<bool>("IsDeleted")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ $image = "docker.stwaddle.com/budget:latest"
|
|||||||
docker build -t $image .
|
docker build -t $image .
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
docker push $image
|
docker push $image
|
||||||
ssh stwaddle_com "./stwaddlecom/update-budget.sh"
|
ssh stwaddle_com "cd stwaddlecom; ./update-budget.sh"
|
||||||
Reference in New Issue
Block a user