using System; using Xunit; using Newtonsoft.Json.Linq; using FluentAssertions; namespace raven_integration { public class QuoteCrud { /// /// 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. /// [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 QuoteSerial = a.ObjectResponse["data"]["serial"].Value(); 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().Should().Be(QuoteId); a.ObjectResponse["data"]["internalReferenceNumber"].Value().Should().Be("INT-001"); var headerConcurrency = a.ObjectResponse["data"]["concurrency"].Value(); // ------------------------------------------------------------------- // ID-FROM-NUMBER LOOKUP // ------------------------------------------------------------------- a = await Util.GetAsync($"quote/id-from-number/{QuoteSerial}", token); Util.ValidateDataReturnResponseOk(a); a.ObjectResponse["data"].Value().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(); QuoteItemId.Should().BeGreaterThan(0); // GET QUOTE ITEM a = await Util.GetAsync($"quote/items/{QuoteItemId}", token); Util.ValidateDataReturnResponseOk(a); a.ObjectResponse["data"]["notes"].Value().Should().Be("Test quote item summary"); var itemConcurrency = a.ObjectResponse["data"]["concurrency"].Value(); // ------------------------------------------------------------------- // 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(); QuoteItemLaborId.Should().BeGreaterThan(0); a.ObjectResponse["data"]["serviceDetails"].Value().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(); 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().Should().Be("INT-002"); var newHeaderConcurrency = a.ObjectResponse["data"]["concurrency"].Value(); 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