public async Task <UploadStatus> DeleteVenueUploadRowForProvider(Guid providerId, int rowNumber)
        {
            using (var dispatcher = _sqlQueryDispatcherFactory.CreateDispatcher(System.Data.IsolationLevel.ReadCommitted))
            {
                await AcquireExclusiveVenueUploadLockForProvider(providerId, dispatcher);

                var venueUpload = await dispatcher.ExecuteQuery(new GetLatestUnpublishedVenueUploadForProvider()
                {
                    ProviderId = providerId
                });

                var(existingRows, lastRowNumber) = await dispatcher.ExecuteQuery(new GetVenueUploadRows()
                {
                    VenueUploadId = venueUpload.VenueUploadId
                });

                var rowToDelete = existingRows.SingleOrDefault(x => x.RowNumber == rowNumber);
                if (rowToDelete == null)
                {
                    throw new ResourceDoesNotExistException(ResourceType.VenueUploadRow, rowNumber);
                }

                if (!rowToDelete.IsDeletable)
                {
                    throw new InvalidStateException(InvalidStateReason.VenueUploadRowCannotBeDeleted);
                }

                var nonDeletedRows = existingRows.Where(x => x.RowNumber != rowNumber).ToArray();

                var rowCollection = new VenueDataUploadRowInfoCollection(
                    lastRowNumber: lastRowNumber,
                    nonDeletedRows
                    .Where(r => r.RowNumber != rowNumber)
                    .Select(r => new VenueDataUploadRowInfo(CsvVenueRow.FromModel(r), r.RowNumber, r.IsSupplementary)));

                var(uploadStatus, _) = await ValidateVenueUploadFile(
                    dispatcher,
                    venueUpload.VenueUploadId,
                    venueUpload.ProviderId,
                    rowCollection);

                await dispatcher.Commit();

                return(uploadStatus);
            }
        }
        public async Task <UploadStatus> UpdateVenueUploadRowForProvider(Guid providerId, int rowNumber, CsvVenueRow updatedRow)
        {
            using var dispatcher = _sqlQueryDispatcherFactory.CreateDispatcher(System.Data.IsolationLevel.ReadCommitted);

            await AcquireExclusiveVenueUploadLockForProvider(providerId, dispatcher);

            var venueUpload = await dispatcher.ExecuteQuery(new GetLatestUnpublishedVenueUploadForProvider()
            {
                ProviderId = providerId
            });

            if (venueUpload == null)
            {
                throw new InvalidStateException(InvalidStateReason.NoUnpublishedVenueUpload);
            }

            if (venueUpload.UploadStatus != UploadStatus.ProcessedWithErrors)
            {
                throw new InvalidUploadStatusException(venueUpload.UploadStatus, UploadStatus.ProcessedWithErrors);
            }

            var(rows, lastRowNumber) = await dispatcher.ExecuteQuery(new GetVenueUploadRows()
            {
                VenueUploadId = venueUpload.VenueUploadId
            });

            var row = rows.SingleOrDefault(r => r.RowNumber == rowNumber);

            if (row == null)
            {
                throw new ResourceDoesNotExistException(ResourceType.VenueUploadRow, rowNumber);
            }

            var updatedRows = new VenueDataUploadRowInfoCollection(
                lastRowNumber: lastRowNumber,
                rows
                .Where(r => r.RowNumber != rowNumber)
                .Select(r => new VenueDataUploadRowInfo(CsvVenueRow.FromModel(r), r.RowNumber, r.IsSupplementary))
                .Append(new VenueDataUploadRowInfo(updatedRow, rowNumber, row.IsSupplementary)));

            var(uploadStatus, _) = await ValidateVenueUploadFile(dispatcher, venueUpload.VenueUploadId, venueUpload.ProviderId, updatedRows);

            await dispatcher.Commit();

            return(uploadStatus);
        }
        public async Task ValidateVenueUploadFile_RowsHasErrors_InsertsExpectedErrorCodesIntoDb(
            CsvVenueRow row,
            IEnumerable <string> expectedErrorCodes,
            IEnumerable <CsvVenueRow> additionalRows)
        {
            // Arrange
            var provider = await TestData.CreateProvider();

            var user = await TestData.CreateUser(providerId : provider.ProviderId);

            var(venueUpload, _) = await TestData.CreateVenueUpload(provider.ProviderId, createdBy : user, UploadStatus.Processing);

            var fileUploadProcessor = new FileUploadProcessor(
                SqlQueryDispatcherFactory,
                Mock.Of <BlobServiceClient>(),
                Clock,
                new RegionCache(SqlQueryDispatcherFactory),
                new ExecuteImmediatelyBackgroundWorkScheduler(Fixture.ServiceScopeFactory));

            var uploadRows = new[] { row }.Concat(additionalRows ?? Enumerable.Empty <CsvVenueRow>()).ToDataUploadRowCollection();

            await WithSqlQueryDispatcher(async dispatcher =>
            {
                // Add a row into Postcodes table to ensure we don't have errors due to it missing
                // (ValidateVenueUploadFile_PostcodeIsNotInDb_InsertsExpectedErrorCodesIntoDb tests that scenario)
                await AddPostcodeInfoForRows(dispatcher, uploadRows);

                // Act
                await fileUploadProcessor.ValidateVenueUploadFile(
                    dispatcher,
                    venueUpload.VenueUploadId,
                    venueUpload.ProviderId,
                    uploadRows);

                var(rows, _) = await dispatcher.ExecuteQuery(new GetVenueUploadRows()
                {
                    VenueUploadId = venueUpload.VenueUploadId
                });

                rows.First().IsValid.Should().BeFalse();
                rows.First().Errors.Should().BeEquivalentTo(expectedErrorCodes);
            });
        }
        public async Task ValidateVenueUploadFile_DoesNotNormalizeInvalidPostcode()
        {
            // Arrange
            var provider = await TestData.CreateProvider();

            var user = await TestData.CreateUser(providerId : provider.ProviderId);

            var(venueUpload, _) = await TestData.CreateVenueUpload(provider.ProviderId, createdBy : user, UploadStatus.Processing);

            var fileUploadProcessor = new FileUploadProcessor(
                SqlQueryDispatcherFactory,
                Mock.Of <BlobServiceClient>(),
                Clock,
                new RegionCache(SqlQueryDispatcherFactory),
                new ExecuteImmediatelyBackgroundWorkScheduler(Fixture.ServiceScopeFactory));

            var row = new CsvVenueRow()
            {
                Postcode = "xxxx",
            };

            var uploadRows = new[] { row }.ToDataUploadRowCollection();

            await WithSqlQueryDispatcher(async dispatcher =>
            {
                // Act
                await fileUploadProcessor.ValidateVenueUploadFile(
                    dispatcher,
                    venueUpload.VenueUploadId,
                    venueUpload.ProviderId,
                    uploadRows);

                var(rows, _) = await dispatcher.ExecuteQuery(new GetVenueUploadRows()
                {
                    VenueUploadId = venueUpload.VenueUploadId
                });

                rows.Count.Should().Be(1);
                rows.Last().Postcode.Should().Be(row.Postcode);
            });
        }
        private async Task <(UploadStatus UploadStatus, IReadOnlyCollection <VenueUploadRow> RevalidatedRows)> RevalidateVenueUploadIfRequired(
            ISqlQueryDispatcher sqlQueryDispatcher,
            Guid venueUploadId)
        {
            var venueUpload = await sqlQueryDispatcher.ExecuteQuery(new GetVenueUpload()
            {
                VenueUploadId = venueUploadId
            });

            if (venueUpload == null)
            {
                throw new ArgumentException("Venue upload does not exist.", nameof(venueUploadId));
            }

            var revalidate = await DoesVenueUploadRequireRevalidating(sqlQueryDispatcher, venueUpload);

            if (!revalidate)
            {
                return(venueUpload.UploadStatus, RevalidatedRows : null);
            }

            var(rows, lastRowNumber) = await sqlQueryDispatcher.ExecuteQuery(new GetVenueUploadRows()
            {
                VenueUploadId = venueUploadId
            });

            var rowCollection = new VenueDataUploadRowInfoCollection(
                lastRowNumber,
                rows.Select(r => new VenueDataUploadRowInfo(CsvVenueRow.FromModel(r), r.RowNumber, r.IsSupplementary)));

            var(uploadStatus, revalidatedRows) = await ValidateVenueUploadFile(
                sqlQueryDispatcher,
                venueUploadId,
                venueUpload.ProviderId,
                rowCollection);

            return(uploadStatus, RevalidatedRows : revalidatedRows);
        }
        // internal for testing
        internal async Task <(UploadStatus uploadStatus, IReadOnlyCollection <VenueUploadRow> Rows)> ValidateVenueUploadFile(
            ISqlQueryDispatcher sqlQueryDispatcher,
            Guid venueUploadId,
            Guid providerId,
            VenueDataUploadRowInfoCollection rows)
        {
            // We need to ensure that any venues that have live offerings attached are not removed when publishing this
            // upload. We do that by adding an additional row to this upload for any venues that are not included in
            // this file that have live offerings attached. This must be done *before* validation so that duplicate
            // checks consider these additional added rows.

            var originalRowCount = rows.Count;

            var existingVenues = await sqlQueryDispatcher.ExecuteQuery(new GetVenueMatchInfoForProvider()
            {
                ProviderId = providerId
            });

            // For each row in the file try to match it to an existing venue
            var rowVenueIdMapping = MatchRowsToExistingVenues(rows, existingVenues);

            // Add a row for any existing venues that are linked to live offerings and haven't been matched
            var matchedVenueIds = rowVenueIdMapping.Where(m => m.HasValue).Select(m => m.Value).ToArray();
            var venuesWithLiveOfferingsNotInFile = existingVenues
                                                   .Where(v => v.HasLiveOfferings && !matchedVenueIds.Contains(v.VenueId))
                                                   .ToArray();

            rows = new VenueDataUploadRowInfoCollection(
                lastRowNumber: rows.LastRowNumber + venuesWithLiveOfferingsNotInFile.Length,
                rows: rows.Concat(
                    venuesWithLiveOfferingsNotInFile.Select((v, i) =>
                                                            new VenueDataUploadRowInfo(CsvVenueRow.FromModel(v), rowNumber: rows.LastRowNumber + i + 1, isSupplementary: true))));

            rowVenueIdMapping = rowVenueIdMapping.Concat(venuesWithLiveOfferingsNotInFile.Select(v => (Guid?)v.VenueId)).ToArray();

            // Grab PostcodeInfo for all of the valid postcodes in the file.
            // We need this for both the validator and to track whether the venue is outside of England
            var allPostcodeInfo = await GetPostcodeInfoForRows(sqlQueryDispatcher, rows);

            var uploadIsValid = true;
            var validator     = new VenueUploadRowValidator(rows, allPostcodeInfo);

            var upsertRecords = new List <SetVenueUploadRowsRecord>();

            for (int i = 0; i < rows.Count; i++)
            {
                var row       = rows[i].Data;
                var rowNumber = rows[i].RowNumber;

                var venueId            = rowVenueIdMapping[i] ?? Guid.NewGuid();
                var isSupplementaryRow = i >= originalRowCount;

                // A row is deletable if it is *not* matched to an existing venue that has attached offerings
                var isDeletable = rowVenueIdMapping[i] is null ||
                                  !existingVenues.Single(v => v.VenueId == rowVenueIdMapping[i]).HasLiveOfferings;

                row.ProviderVenueRef = row.ProviderVenueRef?.Trim();
                row.Postcode         = Postcode.TryParse(row.Postcode, out var postcode) ? postcode : row.Postcode;

                PostcodeInfo postcodeInfo = null;
                if (postcode != null)
                {
                    allPostcodeInfo.TryGetValue(postcode, out postcodeInfo);
                }

                var rowValidationResult = validator.Validate(row);
                var errors     = rowValidationResult.Errors.Select(e => e.ErrorCode).ToArray();
                var rowIsValid = rowValidationResult.IsValid;
                uploadIsValid &= rowIsValid;

                upsertRecords.Add(new SetVenueUploadRowsRecord()
                {
                    RowNumber        = rowNumber,
                    IsValid          = rowIsValid,
                    Errors           = errors,
                    IsSupplementary  = isSupplementaryRow,
                    OutsideOfEngland = postcodeInfo != null ? !postcodeInfo.InEngland : (bool?)null,
                    VenueId          = venueId,
                    IsDeletable      = isDeletable,
                    ProviderVenueRef = row.ProviderVenueRef,
                    VenueName        = row.VenueName,
                    AddressLine1     = row.AddressLine1,
                    AddressLine2     = row.AddressLine2,
                    Town             = row.Town,
                    County           = row.County,
                    Postcode         = row.Postcode,
                    Email            = row.Email,
                    Telephone        = row.Telephone,
                    Website          = row.Website
                });
            }

            var updatedRows = await sqlQueryDispatcher.ExecuteQuery(new SetVenueUploadRows()
            {
                VenueUploadId = venueUploadId,
                ValidatedOn   = _clock.UtcNow,
                Records       = upsertRecords
            });

            await sqlQueryDispatcher.ExecuteQuery(new SetVenueUploadProcessed()
            {
                VenueUploadId         = venueUploadId,
                ProcessingCompletedOn = _clock.UtcNow,
                IsValid = uploadIsValid
            });

            var uploadStatus = uploadIsValid ? UploadStatus.ProcessedSuccessfully : UploadStatus.ProcessedWithErrors;

            return(uploadStatus, updatedRows);
        }
        public async Task ValidateVenueUploadFile_InsertsRowsIntoDb()
        {
            // Arrange
            var provider = await TestData.CreateProvider();

            var user = await TestData.CreateUser(providerId : provider.ProviderId);

            var(venueUpload, _) = await TestData.CreateVenueUpload(provider.ProviderId, createdBy : user, UploadStatus.Processing);

            var fileUploadProcessor = new FileUploadProcessor(
                SqlQueryDispatcherFactory,
                Mock.Of <BlobServiceClient>(),
                Clock,
                new RegionCache(SqlQueryDispatcherFactory),
                new ExecuteImmediatelyBackgroundWorkScheduler(Fixture.ServiceScopeFactory));

            var row = new CsvVenueRow()
            {
                ProviderVenueRef = "REF",
                VenueName        = "Place",
                AddressLine1     = "Line 1",
                AddressLine2     = "Line 2",
                Town             = "Town",
                County           = "County",
                Postcode         = "AB1 2DE",
                Email            = "*****@*****.**",
                Telephone        = "01234 567890",
                Website          = "provider.com/place"
            };

            var uploadRows = new[] { row }.ToDataUploadRowCollection();

            await WithSqlQueryDispatcher(async dispatcher =>
            {
                // Act
                await fileUploadProcessor.ValidateVenueUploadFile(
                    dispatcher,
                    venueUpload.VenueUploadId,
                    venueUpload.ProviderId,
                    uploadRows);

                var(rows, _) = await dispatcher.ExecuteQuery(new GetVenueUploadRows()
                {
                    VenueUploadId = venueUpload.VenueUploadId
                });

                rows.Count.Should().Be(1);
                rows.Last().Should().BeEquivalentTo(new VenueUploadRow()
                {
                    RowNumber        = 2,
                    LastUpdated      = Clock.UtcNow,
                    LastValidated    = Clock.UtcNow,
                    IsSupplementary  = false,
                    IsDeletable      = true,
                    AddressLine1     = row.AddressLine1,
                    AddressLine2     = row.AddressLine2,
                    County           = row.County,
                    Email            = row.Email,
                    Postcode         = row.Postcode,
                    ProviderVenueRef = row.ProviderVenueRef,
                    Telephone        = row.Telephone,
                    Town             = row.Town,
                    VenueName        = row.VenueName,
                    Website          = row.Website
                }, config => config.Excluding(r => r.IsValid).Excluding(r => r.Errors).Excluding(r => r.VenueId));
            });
        }