fix(money): движения смены попадают в остатки счетов + запрет отрицательного остатка
Раньше продажи/возвраты/внесения/выплаты/расхождение смены не влияли на остаток кассы (касса уходила в −, эквайринг оставался 0). Теперь при закрытии смены MoneyService.MaterializeShiftMovementsAsync создаёт Posted-движения money_transactions по типам (Category): продажи нал/карта, возвраты нал/карта, внесение, выплата, излишек/недостача — привязанные к смене, идемпотентно. Остаток кассы/эквайринга и отчёт «Деньги» теперь верны автоматически (всё читает money_transactions). Отрицательный остаток: расход (CashOut/PaymentOut/TransferOut) больше остатка счёта и выплата из смены больше наличных смены → 409 insufficient-funds; настройка Organization.AllowNegativeBalance (по умолч. выкл., меняет только Admin). Миграция Phase34 (idempotent): money_transactions.Category/.CashShiftId, organizations.AllowNegativeBalance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b6c4d6056d
commit
1e6d7fb2ab
|
|
@ -166,7 +166,13 @@ public async Task<ActionResult<object>> Drawer(Guid id, [FromBody] DrawerOpInput
|
|||
var userId = CurrentUserId();
|
||||
var r = await _money.AddDrawerOperationAsync(id, input.Direction, input.Amount, input.Reason, userId, ct);
|
||||
if (r.Status != MoneyServiceStatus.Ok)
|
||||
return Problem(r.Message, statusCode: r.Status == MoneyServiceStatus.NotFound ? 404 : 400,
|
||||
return Problem(r.Message,
|
||||
statusCode: r.Status switch
|
||||
{
|
||||
MoneyServiceStatus.NotFound => 404,
|
||||
MoneyServiceStatus.InsufficientFunds => 409,
|
||||
_ => 400,
|
||||
},
|
||||
type: $"https://food-market.kz/errors/{r.ErrorCode}");
|
||||
return Ok(new { id = r.OperationId });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public record MoneyTxLinkRow(Guid? SupplyId, string? SupplyNumber, Guid? Supplie
|
|||
public record MoneyTxRow(
|
||||
Guid Id, string Number, DateTime Date,
|
||||
MoneyTransactionKind Kind, MoneyTransactionStatus Status,
|
||||
MoneyTransactionCategory Category,
|
||||
Guid AccountId, string AccountName,
|
||||
Guid? CounterpartyId, string? CounterpartyName,
|
||||
Guid? ExpenseItemId, string? ExpenseItemName,
|
||||
|
|
@ -100,6 +101,7 @@ public record MoneyTxInput(
|
|||
.Select(x => new
|
||||
{
|
||||
x.Id, x.Number, x.Date, x.Kind, x.Status,
|
||||
x.Category,
|
||||
x.AccountId, AccountName = _db.MoneyAccounts.Where(a => a.Id == x.AccountId).Select(a => a.Name).FirstOrDefault() ?? "",
|
||||
x.CounterpartyId, CounterpartyName = x.CounterpartyId == null ? null : _db.Counterparties.Where(c => c.Id == x.CounterpartyId).Select(c => c.Name).FirstOrDefault(),
|
||||
x.ExpenseItemId, ExpenseItemName = x.ExpenseItemId == null ? null : _db.ExpenseItems.Where(e => e.Id == x.ExpenseItemId).Select(e => e.Name).FirstOrDefault(),
|
||||
|
|
@ -124,6 +126,7 @@ public record MoneyTxInput(
|
|||
|
||||
var items = rows.Select(r => new MoneyTxRow(
|
||||
r.Id, r.Number, r.Date, r.Kind, r.Status,
|
||||
r.Category,
|
||||
r.AccountId, r.AccountName, r.CounterpartyId, r.CounterpartyName,
|
||||
r.ExpenseItemId, r.ExpenseItemName,
|
||||
r.CurrencyId, r.CurrencyCode,
|
||||
|
|
@ -340,6 +343,7 @@ private Guid CurrentUserId()
|
|||
MoneyServiceStatus.LinkedSumMismatch => BadRequest(new { code = r.ErrorCode, message = r.Message }),
|
||||
MoneyServiceStatus.WrongCurrency => BadRequest(new { code = r.ErrorCode, message = r.Message }),
|
||||
MoneyServiceStatus.AccountInactive => Conflict(new { code = r.ErrorCode, message = r.Message }),
|
||||
MoneyServiceStatus.InsufficientFunds => Conflict(new { code = r.ErrorCode, message = r.Message }),
|
||||
_ => BadRequest(new { code = r.ErrorCode, message = r.Message }),
|
||||
};
|
||||
|
||||
|
|
@ -360,6 +364,7 @@ private Guid CurrentUserId()
|
|||
.Select(x => Microsoft.EntityFrameworkCore.EF.Property<uint>(x, "xmin")).FirstAsync(ct);
|
||||
return new MoneyTxRow(
|
||||
mt.Id, mt.Number, mt.Date, mt.Kind, mt.Status,
|
||||
mt.Category,
|
||||
mt.AccountId, account, mt.CounterpartyId, counterparty,
|
||||
mt.ExpenseItemId, exp,
|
||||
mt.CurrencyId, currency,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public record OrgSettingsDto(
|
|||
bool ShowMarkedOnProduct,
|
||||
bool ShowMinMaxStock,
|
||||
bool AllowFractionalPrices,
|
||||
bool AllowNegativeBalance,
|
||||
bool ShowReferencePriceOnProduct,
|
||||
bool ShowCountryOfOriginOnProduct,
|
||||
bool ShowDescriptionOnProduct);
|
||||
|
|
@ -49,6 +50,7 @@ public record OrgSettingsInput(
|
|||
bool ShowMarkedOnProduct,
|
||||
bool ShowMinMaxStock,
|
||||
bool AllowFractionalPrices,
|
||||
bool AllowNegativeBalance,
|
||||
bool ShowReferencePriceOnProduct,
|
||||
bool ShowCountryOfOriginOnProduct,
|
||||
bool ShowDescriptionOnProduct);
|
||||
|
|
@ -87,6 +89,7 @@ public async Task<ActionResult<OrgSettingsDto>> Update([FromBody] OrgSettingsInp
|
|||
o.ShowMarkedOnProduct = input.ShowMarkedOnProduct;
|
||||
o.ShowMinMaxStock = input.ShowMinMaxStock;
|
||||
o.AllowFractionalPrices = input.AllowFractionalPrices;
|
||||
o.AllowNegativeBalance = input.AllowNegativeBalance;
|
||||
o.ShowReferencePriceOnProduct = input.ShowReferencePriceOnProduct;
|
||||
o.ShowCountryOfOriginOnProduct = input.ShowCountryOfOriginOnProduct;
|
||||
o.ShowDescriptionOnProduct = input.ShowDescriptionOnProduct;
|
||||
|
|
@ -118,6 +121,7 @@ private async Task<decimal> ReadVatRateAsync(string countryCode, CancellationTok
|
|||
o.ShowMarkedOnProduct,
|
||||
o.ShowMinMaxStock,
|
||||
o.AllowFractionalPrices,
|
||||
o.AllowNegativeBalance,
|
||||
o.ShowReferencePriceOnProduct,
|
||||
o.ShowCountryOfOriginOnProduct,
|
||||
o.ShowDescriptionOnProduct);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ public enum MoneyServiceStatus
|
|||
LinkedSumMismatch = 9,
|
||||
WrongCurrency = 10,
|
||||
AccountInactive = 11,
|
||||
InsufficientFunds = 12,
|
||||
}
|
||||
|
||||
public record MoneyServiceResult(MoneyServiceStatus Status, string? ErrorCode = null, string? Message = null)
|
||||
|
|
|
|||
|
|
@ -53,10 +53,33 @@ public enum MoneyTransactionStatus
|
|||
/// отмена = отдельный флаг <see cref="Status"/>=Draft + пересчёт
|
||||
/// <c>Supply.PayedSum</c> в сторону уменьшения, в одной транзакции
|
||||
/// Postgres (см. <c>MoneyService.Unpost</c>).</summary>
|
||||
/// <summary>Происхождение/тип движения денег — для отчёта «Деньги» и для того,
|
||||
/// чтобы отличать движения смены (создаются при закрытии кассовой смены) от
|
||||
/// ручных платежей/ордеров. Manual — обычный платёж/ордер/перевод.</summary>
|
||||
public enum MoneyTransactionCategory
|
||||
{
|
||||
Manual = 0,
|
||||
RetailSalesCash = 1, // розничные продажи наличными (приход в кассу смены)
|
||||
RetailReturnsCash = 2, // возвраты покупателям наличными (расход из кассы смены)
|
||||
DrawerIn = 3, // внесение в кассу смены
|
||||
DrawerOut = 4, // выплата из кассы смены
|
||||
ShiftSurplus = 5, // излишек при закрытии смены (факт > должно быть)
|
||||
ShiftShortage = 6, // недостача при закрытии смены (факт < должно быть)
|
||||
RetailSalesCard = 7, // продажи картой (приход на эквайринг смены)
|
||||
RetailReturnsCard = 8, // возвраты по карте (расход с эквайринга смены)
|
||||
}
|
||||
|
||||
public class MoneyTransaction : TenantEntity, IVersionedEntity
|
||||
{
|
||||
public uint Xmin { get; set; }
|
||||
|
||||
/// <summary>Тип движения. Manual — ручной платёж/ордер/перевод; остальные —
|
||||
/// движения кассовой смены (материализуются при закрытии).</summary>
|
||||
public MoneyTransactionCategory Category { get; set; } = MoneyTransactionCategory.Manual;
|
||||
|
||||
/// <summary>Смена, породившая это движение (для движений смены). null — ручной документ.</summary>
|
||||
public Guid? CashShiftId { get; set; }
|
||||
|
||||
/// <summary>Человекочитаемый номер, уникальный в орге. Генерим по шаблону
|
||||
/// <c>Д-{year}-{seq6}</c> (Д как «деньги»), общий счётчик на все виды
|
||||
/// MoneyTransaction — чтобы в списке «Платежи и ордера» документы шли
|
||||
|
|
|
|||
|
|
@ -73,6 +73,11 @@ public class Organization : Entity
|
|||
/// дробное через API.</summary>
|
||||
public bool AllowFractionalPrices { get; set; }
|
||||
|
||||
/// <summary>Разрешить ли уходить счетам/кассам в минус. По умолчанию выкл.:
|
||||
/// расход больше остатка (оплата, выплата из смены) отклоняется с 409.
|
||||
/// Менять может только администратор организации.</summary>
|
||||
public bool AllowNegativeBalance { get; set; }
|
||||
|
||||
/// <summary>Показывать ли в карточке товара поле «Эталонная цена».
|
||||
/// Default: true.</summary>
|
||||
public bool ShowReferencePriceOnProduct { get; set; } = true;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,21 @@ public async Task<MoneyServiceResult> PostAsync(Guid transactionId, Guid current
|
|||
return MoneyServiceResult.Fail(MoneyServiceStatus.ValidationFailed, "amount-must-be-positive",
|
||||
"Сумма платежа должна быть больше нуля.");
|
||||
|
||||
// Отрицательный остаток: расход больше остатка счёта → отказ,
|
||||
// если орг не разрешила уходить в минус.
|
||||
if (mt.Kind is MoneyTransactionKind.PaymentOut or MoneyTransactionKind.CashOut or MoneyTransactionKind.TransferOut)
|
||||
{
|
||||
var allowNeg = await _db.Organizations.IgnoreQueryFilters()
|
||||
.Where(o => o.Id == mt.OrganizationId).Select(o => o.AllowNegativeBalance).FirstOrDefaultAsync(ct);
|
||||
if (!allowNeg)
|
||||
{
|
||||
var balance = await AccountBalanceAsync(mt.AccountId, mt.Id, ct);
|
||||
if (balance < mt.Amount - MoneyMath.Eps)
|
||||
return MoneyServiceResult.Fail(MoneyServiceStatus.InsufficientFunds, "insufficient-funds",
|
||||
$"На счёте «{mt.Account.Name}» недостаточно денег: остаток {balance:0.##} ₸, нужно {mt.Amount:0.##} ₸.");
|
||||
}
|
||||
}
|
||||
|
||||
// Сумма связей не может превышать сумму документа.
|
||||
var linkedTotal = mt.Links.Sum(l => l.LinkedSum);
|
||||
if (linkedTotal > mt.Amount + 0.0001m)
|
||||
|
|
@ -296,6 +311,11 @@ public async Task<MoneyServiceResult> UnpostAsync(Guid transactionId, Guid curre
|
|||
shift.Notes = notes;
|
||||
shift.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Материализуем движения смены в money_transactions → остаток счетов и
|
||||
// отчёт «Деньги» теперь учитывают продажи/возвраты/внесения/выплаты/расхождение.
|
||||
await MaterializeShiftMovementsAsync(shift, salesCash, salesCard, returnsCash, returnsCard,
|
||||
drawerIn, drawerOut, shift.CashDifference, ct);
|
||||
|
||||
await _db.SaveChangesAsync(ct);
|
||||
await tx.CommitAsync(ct);
|
||||
_log.LogInformation("CashShift {Number} closed by {UserId}: expected={Expected} actual={Actual} diff={Diff}",
|
||||
|
|
@ -321,6 +341,20 @@ public async Task<MoneyServiceResult> UnpostAsync(Guid transactionId, Guid curre
|
|||
return new(MoneyServiceStatus.ShiftNotOpen, null,
|
||||
"shift-not-open", "Смена закрыта — добавлять операции нельзя.");
|
||||
|
||||
// Выплата больше наличных в кассе смены → отказ (если орг не разрешает минус).
|
||||
if (direction == CashDrawerDirection.Out)
|
||||
{
|
||||
var allowNeg = await _db.Organizations.IgnoreQueryFilters()
|
||||
.Where(o => o.Id == shift.OrganizationId).Select(o => o.AllowNegativeBalance).FirstOrDefaultAsync(ct);
|
||||
if (!allowNeg)
|
||||
{
|
||||
var cashOnHand = await ShiftCashOnHandAsync(shift, ct);
|
||||
if (cashOnHand < amount - MoneyMath.Eps)
|
||||
return new(MoneyServiceStatus.InsufficientFunds, null, "insufficient-funds",
|
||||
$"В кассе смены недостаточно наличных: есть {cashOnHand:0.##} ₸, нужно {amount:0.##} ₸.");
|
||||
}
|
||||
}
|
||||
|
||||
var op = new CashDrawerOperation
|
||||
{
|
||||
OrganizationId = shift.OrganizationId,
|
||||
|
|
@ -335,4 +369,91 @@ public async Task<MoneyServiceResult> UnpostAsync(Guid transactionId, Guid curre
|
|||
await _db.SaveChangesAsync(ct);
|
||||
return new(MoneyServiceStatus.Ok, op.Id, null, null);
|
||||
}
|
||||
|
||||
/// <summary>Остаток счёта = OpeningBalance + Σ приходов − Σ расходов по
|
||||
/// проведённым movements (кроме исключённого документа).</summary>
|
||||
private async Task<decimal> AccountBalanceAsync(Guid accountId, Guid? excludeTxId, CancellationToken ct)
|
||||
{
|
||||
var opening = await _db.MoneyAccounts.Where(a => a.Id == accountId)
|
||||
.Select(a => a.OpeningBalance).FirstAsync(ct);
|
||||
var q = _db.MoneyTransactions
|
||||
.Where(m => m.AccountId == accountId && m.Status == MoneyTransactionStatus.Posted);
|
||||
if (excludeTxId is Guid ex) q = q.Where(m => m.Id != ex);
|
||||
var rows = await q.Select(m => new { m.Kind, m.Amount }).ToListAsync(ct);
|
||||
var inSum = rows.Where(r => r.Kind is MoneyTransactionKind.PaymentIn or MoneyTransactionKind.CashIn or MoneyTransactionKind.TransferIn).Sum(r => r.Amount);
|
||||
var outSum = rows.Where(r => r.Kind is MoneyTransactionKind.PaymentOut or MoneyTransactionKind.CashOut or MoneyTransactionKind.TransferOut).Sum(r => r.Amount);
|
||||
return MoneyMath.AccountBalance(opening, inSum, outSum);
|
||||
}
|
||||
|
||||
/// <summary>Наличные в кассе открытой смены: размен + продажи нал − возвраты
|
||||
/// нал + внесения − выплаты (на текущий момент).</summary>
|
||||
private async Task<decimal> ShiftCashOnHandAsync(CashShift shift, CancellationToken ct)
|
||||
{
|
||||
var sales = await _db.RetailSales
|
||||
.Where(s => s.CashShiftId == shift.Id && s.Status == RetailSaleStatus.Posted)
|
||||
.Select(s => new { s.IsReturn, s.PaidCash }).ToListAsync(ct);
|
||||
var salesCash = sales.Where(s => !s.IsReturn).Sum(s => s.PaidCash);
|
||||
var returnsCash = sales.Where(s => s.IsReturn).Sum(s => s.PaidCash);
|
||||
var ops = await _db.CashDrawerOperations.Where(o => o.CashShiftId == shift.Id)
|
||||
.Select(o => new { o.Direction, o.Amount }).ToListAsync(ct);
|
||||
var drawerIn = ops.Where(o => o.Direction == CashDrawerDirection.In).Sum(o => o.Amount);
|
||||
var drawerOut = ops.Where(o => o.Direction == CashDrawerDirection.Out).Sum(o => o.Amount);
|
||||
return shift.OpeningFloat + salesCash - returnsCash + drawerIn - drawerOut;
|
||||
}
|
||||
|
||||
/// <summary>Материализует финансовые движения закрытой смены в money_transactions
|
||||
/// (Posted) — продажи/возвраты нал (касса), внесения/выплаты (касса),
|
||||
/// излишек/недостача (касса), продажи/возвраты по карте (эквайринг).
|
||||
/// Идемпотентно по CashShiftId. Размен (OpeningFloat) не материализуется —
|
||||
/// эти деньги уже были в кассе до смены.</summary>
|
||||
private async Task MaterializeShiftMovementsAsync(CashShift shift,
|
||||
decimal salesCash, decimal salesCard, decimal returnsCash, decimal returnsCard,
|
||||
decimal drawerIn, decimal drawerOut, decimal difference, CancellationToken ct)
|
||||
{
|
||||
var existing = await _db.MoneyTransactions.Where(m => m.CashShiftId == shift.Id).ToListAsync(ct);
|
||||
if (existing.Count > 0) _db.MoneyTransactions.RemoveRange(existing);
|
||||
|
||||
var year = (shift.ClosedAt ?? DateTime.UtcNow).Year;
|
||||
var baseNum = await MoneyNumbering.NextMoneyTxNumberAsync(_db, shift.OrganizationId, year, ct);
|
||||
var seq = int.Parse(baseNum.Split('-')[2]);
|
||||
var cashCur = await _db.MoneyAccounts.Where(a => a.Id == shift.CashAccountId).Select(a => a.CurrencyId).FirstAsync(ct);
|
||||
Guid? acqId = shift.AcquiringAccountId;
|
||||
var acqCur = acqId is Guid a ? await _db.MoneyAccounts.Where(x => x.Id == a).Select(x => x.CurrencyId).FirstAsync(ct) : Guid.Empty;
|
||||
|
||||
void Add(Guid accountId, Guid currencyId, MoneyTransactionKind kind, MoneyTransactionCategory cat, decimal amount, string purpose)
|
||||
{
|
||||
if (amount <= MoneyMath.Eps) return;
|
||||
_db.MoneyTransactions.Add(new MoneyTransaction
|
||||
{
|
||||
OrganizationId = shift.OrganizationId,
|
||||
Number = $"Д-{year}-{seq++:D6}",
|
||||
Date = shift.ClosedAt ?? DateTime.UtcNow,
|
||||
Kind = kind,
|
||||
Category = cat,
|
||||
Status = MoneyTransactionStatus.Posted,
|
||||
AccountId = accountId,
|
||||
CurrencyId = currencyId,
|
||||
Amount = amount,
|
||||
CashShiftId = shift.Id,
|
||||
Purpose = $"{purpose} (смена {shift.Number})",
|
||||
PostedAt = DateTime.UtcNow,
|
||||
PostedByUserId = shift.ClosedByUserId,
|
||||
});
|
||||
}
|
||||
|
||||
Add(shift.CashAccountId, cashCur, MoneyTransactionKind.CashIn, MoneyTransactionCategory.RetailSalesCash, salesCash, "Розничные продажи (нал)");
|
||||
Add(shift.CashAccountId, cashCur, MoneyTransactionKind.CashOut, MoneyTransactionCategory.RetailReturnsCash, returnsCash, "Возвраты покупателям (нал)");
|
||||
Add(shift.CashAccountId, cashCur, MoneyTransactionKind.CashIn, MoneyTransactionCategory.DrawerIn, drawerIn, "Внесение в кассу");
|
||||
Add(shift.CashAccountId, cashCur, MoneyTransactionKind.CashOut, MoneyTransactionCategory.DrawerOut, drawerOut, "Выплата из кассы");
|
||||
if (difference > MoneyMath.Eps)
|
||||
Add(shift.CashAccountId, cashCur, MoneyTransactionKind.CashIn, MoneyTransactionCategory.ShiftSurplus, difference, "Излишек смены");
|
||||
else if (difference < -MoneyMath.Eps)
|
||||
Add(shift.CashAccountId, cashCur, MoneyTransactionKind.CashOut, MoneyTransactionCategory.ShiftShortage, -difference, "Недостача смены");
|
||||
|
||||
if (acqId is Guid aid)
|
||||
{
|
||||
Add(aid, acqCur, MoneyTransactionKind.PaymentIn, MoneyTransactionCategory.RetailSalesCard, salesCard, "Продажи картой");
|
||||
Add(aid, acqCur, MoneyTransactionKind.PaymentOut, MoneyTransactionCategory.RetailReturnsCard, returnsCard, "Возвраты по карте");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ public static void ConfigureMoney(this ModelBuilder b)
|
|||
e.HasIndex(x => new { x.OrganizationId, x.AccountId, x.Date });
|
||||
e.HasIndex(x => new { x.OrganizationId, x.CounterpartyId })
|
||||
.HasFilter("\"CounterpartyId\" IS NOT NULL");
|
||||
// Движения смены (материализуются при закрытии) ищем по смене —
|
||||
// идемпотентность и отчёт по смене.
|
||||
e.HasIndex(x => x.CashShiftId).HasFilter("\"CashShiftId\" IS NOT NULL");
|
||||
});
|
||||
|
||||
b.Entity<MoneyTransactionLink>(e =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <summary>Phase34 — движения кассовой смены попадают в деньги.
|
||||
///
|
||||
/// Добавляет:
|
||||
/// <list type="bullet">
|
||||
/// <item>money_transactions.<c>Category</c> (integer, DEFAULT 0 = Manual) —
|
||||
/// тип движения: розничные продажи/возвраты нал/карта, внесения/выплаты,
|
||||
/// излишек/недостача смены.</item>
|
||||
/// <item>money_transactions.<c>CashShiftId</c> (uuid, nullable) —
|
||||
/// смена, породившая движение (материализуется при закрытии).</item>
|
||||
/// <item>organizations.<c>AllowNegativeBalance</c> (boolean, DEFAULT false) —
|
||||
/// разрешить ли уход счетов в минус.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// Идемпотентно (IF NOT EXISTS). Данные не пересчитываем: остаток счёта
|
||||
/// вычисляемый (Opening + Σ Posted money_transactions); движения прошлых
|
||||
/// закрытых смен материализуются ретроспективно отдельным backfill-методом
|
||||
/// в DevDataSeeder (idempotent, по CashShiftId).</summary>
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20261008030000_Phase34_ShiftMoneyMovements")]
|
||||
public partial class Phase34_ShiftMoneyMovements : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
b.Sql(@"
|
||||
ALTER TABLE public.money_transactions
|
||||
ADD COLUMN IF NOT EXISTS ""Category"" integer NOT NULL DEFAULT 0;
|
||||
ALTER TABLE public.money_transactions
|
||||
ADD COLUMN IF NOT EXISTS ""CashShiftId"" uuid;
|
||||
CREATE INDEX IF NOT EXISTS ""IX_money_transactions_CashShiftId""
|
||||
ON public.money_transactions (""CashShiftId"")
|
||||
WHERE ""CashShiftId"" IS NOT NULL;
|
||||
ALTER TABLE public.organizations
|
||||
ADD COLUMN IF NOT EXISTS ""AllowNegativeBalance"" boolean NOT NULL DEFAULT false;
|
||||
");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
b.Sql(@"
|
||||
DROP INDEX IF EXISTS public.""IX_money_transactions_CashShiftId"";
|
||||
ALTER TABLE public.money_transactions DROP COLUMN IF EXISTS ""CashShiftId"";
|
||||
ALTER TABLE public.money_transactions DROP COLUMN IF EXISTS ""Category"";
|
||||
ALTER TABLE public.organizations DROP COLUMN IF EXISTS ""AllowNegativeBalance"";
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue