This commit is contained in:
2026-02-28 19:53:34 -08:00
parent 59cb886adb
commit db593886a3
7 changed files with 716 additions and 17 deletions

134
Quote/QuoteCrud.cs Normal file
View File

@@ -0,0 +1,134 @@
using System;
using Xunit;
using Newtonsoft.Json.Linq;
using FluentAssertions;
namespace raven_integration
{
public class QuoteCrud
{
/// <summary>
/// Full CRUD for a Quote header + QuoteItem + QuoteItemLabor sub-type.
/// Also tests concurrency violation on the header and id-from-number lookup.
/// Mirrors the WorkOrderCrud structure — if a refactor consolidates WO/Quote
/// patterns, failures here flag the regression.
/// </summary>
[Fact]
public async Task CRUD()
{
var token = await Util.GetTokenAsync("BizAdmin");
var isoNow = DateTime.UtcNow.ToString("o");
var isoOneHourFromNow = DateTime.UtcNow.AddHours(1).ToString("o");
// -------------------------------------------------------------------
// CREATE QUOTE HEADER
// customerId=1 is a seeded customer; serial=0 means server assigns it.
// -------------------------------------------------------------------
var payload = $$"""
{"id":0,"concurrency":0,"serial":0,"notes":"Test quote the quick brown fox jumped over the six lazy dogs!","wiki":null,"customFields":"{}","tags":[],"preparedById":null,"introduction":"Test introduction","requested":null,"validUntil":null,"submitted":null,"approved":null,"customerId":1,"projectId":null,"internalReferenceNumber":"INT-001","customerReferenceNumber":null,"customerContactName":null,"onsite":true,"contractId":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
ApiResponse a = await Util.PostAsync("quote", token, payload);
Util.ValidateDataReturnResponseOk(a);
long QuoteId = a.ObjectResponse["data"]["id"].Value<long>();
long QuoteSerial = a.ObjectResponse["data"]["serial"].Value<long>();
QuoteId.Should().BeGreaterThan(0);
QuoteSerial.Should().BeGreaterThan(0, "server should have assigned a non-zero serial");
// -------------------------------------------------------------------
// GET QUOTE
// -------------------------------------------------------------------
a = await Util.GetAsync($"quote/{QuoteId}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["id"].Value<long>().Should().Be(QuoteId);
a.ObjectResponse["data"]["internalReferenceNumber"].Value<string>().Should().Be("INT-001");
var headerConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// -------------------------------------------------------------------
// ID-FROM-NUMBER LOOKUP
// -------------------------------------------------------------------
a = await Util.GetAsync($"quote/id-from-number/{QuoteSerial}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"].Value<long>().Should().Be(QuoteId);
// -------------------------------------------------------------------
// CREATE QUOTE ITEM
// -------------------------------------------------------------------
payload = $$"""
{"id":0,"concurrency":0,"notes":"Test quote item summary","wiki":null,"customFields":"{}","tags":[],"quoteId":{{QuoteId}},"techNotes":"Tech notes for item","workOrderItemStatusId":null,"workOrderItemPriorityId":null,"requestDate":null,"warrantyService":false,"sequence":1}
""";
a = await Util.PostAsync("quote/items", token, payload);
Util.ValidateDataReturnResponseOk(a);
long QuoteItemId = a.ObjectResponse["data"]["id"].Value<long>();
QuoteItemId.Should().BeGreaterThan(0);
// GET QUOTE ITEM
a = await Util.GetAsync($"quote/items/{QuoteItemId}", token);
Util.ValidateDataReturnResponseOk(a);
a.ObjectResponse["data"]["notes"].Value<string>().Should().Be("Test quote item summary");
var itemConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
// -------------------------------------------------------------------
// CREATE QUOTE ITEM LABOR (nested sub-type)
// -------------------------------------------------------------------
payload = $$"""
{"id":0,"concurrency":0,"userId":null,"serviceStartDate":"{{isoNow}}","serviceStopDate":"{{isoOneHourFromNow}}","serviceRateId":null,"serviceDetails":"Test labor service details","serviceRateQuantity":1,"noChargeQuantity":0,"taxCodeSaleId":null,"priceOverride":null,"quoteItemId":{{QuoteItemId}}}
""";
a = await Util.PostAsync("quote/items/labors", token, payload);
Util.ValidateDataReturnResponseOk(a);
long QuoteItemLaborId = a.ObjectResponse["data"]["id"].Value<long>();
QuoteItemLaborId.Should().BeGreaterThan(0);
a.ObjectResponse["data"]["serviceDetails"].Value<string>().Should().Be("Test labor service details");
// -------------------------------------------------------------------
// UPDATE QUOTE ITEM
// -------------------------------------------------------------------
payload = $$"""
{"id":{{QuoteItemId}},"concurrency":{{itemConcurrency}},"notes":"Updated item notes","wiki":null,"customFields":"{}","tags":[],"quoteId":{{QuoteId}},"techNotes":"Updated tech notes","workOrderItemStatusId":null,"workOrderItemPriorityId":null,"requestDate":null,"warrantyService":false,"sequence":1}
""";
a = await Util.PutAsync("quote/items/", token, payload);
Util.ValidateHTTPStatusCode(a, 200);
var newItemConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newItemConcurrency.Should().NotBe(itemConcurrency);
// CONCURRENCY VIOLATION on item
a = await Util.PutAsync("quote/items/", token, payload); // still has stale concurrency
Util.ValidateConcurrencyError(a);
// -------------------------------------------------------------------
// UPDATE QUOTE HEADER
// -------------------------------------------------------------------
payload = $$"""
{"id":{{QuoteId}},"concurrency":{{headerConcurrency}},"serial":{{QuoteSerial}},"notes":"Updated quote notes","wiki":null,"customFields":"{}","tags":[],"preparedById":null,"introduction":"Updated introduction","requested":null,"validUntil":null,"submitted":null,"approved":null,"customerId":1,"projectId":null,"internalReferenceNumber":"INT-002","customerReferenceNumber":null,"customerContactName":null,"onsite":false,"contractId":null,"postAddress":null,"postCity":null,"postRegion":null,"postCountry":null,"postCode":null,"address":null,"city":null,"region":null,"country":null,"addressPostal":null,"latitude":null,"longitude":null}
""";
a = await Util.PutAsync("quote", token, payload);
Util.ValidateHTTPStatusCode(a, 200);
a.ObjectResponse["data"]["internalReferenceNumber"].Value<string>().Should().Be("INT-002");
var newHeaderConcurrency = a.ObjectResponse["data"]["concurrency"].Value<uint>();
newHeaderConcurrency.Should().NotBe(headerConcurrency);
// CONCURRENCY VIOLATION on header
a = await Util.PutAsync("quote", token, payload); // stale concurrency
Util.ValidateConcurrencyError(a);
// -------------------------------------------------------------------
// DELETE in bottom-up order: labor → item → header
// -------------------------------------------------------------------
a = await Util.DeleteAsync($"quote/items/labors/{QuoteItemLaborId}", token);
Util.ValidateHTTPStatusCode(a, 204);
a = await Util.DeleteAsync($"quote/items/{QuoteItemId}", token);
Util.ValidateHTTPStatusCode(a, 204);
a = await Util.DeleteAsync($"quote/{QuoteId}", token);
Util.ValidateHTTPStatusCode(a, 204);
// Confirm header is gone
a = await Util.GetAsync($"quote/{QuoteId}", token);
Util.ValidateResponseNotFound(a);
}
}//eoc
}//eons