Podrška

DTO Flow centar za pomoć

Praktične upute za pokretanje partnerskih razmjena, uspješan rad partnera, povezivanje sustava i dobivanje podrške kada je trebate.

Integracija

Automatizacija

Povežite pouzdane sustave kada se redovite datoteke trebaju automatski predavati, provjeravati ili preuzimati bez dodatnih ručnih koraka za vaš tim.

Istražite API predaju datoteka za partnerske feedovePregledajte sigurnost povezanih razmjena

Podijeljeni feedovi i Subfeedovi: postavljanje, pristup i rješavanje poteškoća

Pristup

Privatni pristupni tokeni

Privatni pristupni tokeni su vjerodajnice ograničenog opsega za skripte, automatizirane zadatke i povezane partnerske sustave. Izradite token samo za feed i radnju koju automatizacija treba, odmah pohranite tajnu i opozovite stare tokene kada se promijeni vlasnik, poslužitelj ili integracija.

  • Koristite jedan token po skripti, planiranom poslu, partnerskom sustavu ili okruženju izvođenja.
  • Dodijelite samo potrebne opsege, primjerice provjeru razdoblja, predaju ili preuzimanje.
  • Pohranite tajnu u upravitelj lozinki, CI/CD tajnu, sigurno spremište ili spremište tajni planera.
  • Rotirajte tokene prema planiranom rasporedu i odmah nakon promjene vlasništva pristupa.
  • Pregledajte zapise feeda kada istražujete koji je token provjerio, prenio ili preuzeo datoteke.

Privatne pristupne tokene tretirajte kao lozinke. DTO Flow prikazuje punu vrijednost tajne samo jednom kako bi je sustav koji će je koristiti mogao sigurno pohraniti.

Pronađite odredište i pratite točnu predaju

GetConnectionAsync vraća sigurne podatke o organizaciji i tokenu, isteku, povezanom partneru, ovlastima organizacije, načinu pristupa feedovima i podržanim mogućnostima. Ne vraća vjerodajnice. ListAccessibleFeedsAsync vraća ID, ključ, naziv, smjer, status, način podjele i efektivne ovlasti feeda. Stranice počinju od 1, sadrže do 1.000 stavki i uključuju HasNextPage; prazan katalog valjan je odgovor.

ListSubfeedsAsync vraća odredišta kojima veza trenutačno ima pristup. Tokeni organizacije mogu pronaći subfeedove bez primatelja; partnerovi tokeni vide samo svoju trenutačnu dodjelu. Manifest opisuje očekivane datoteke, raspored, način podjele, MaxFileCount i MaxFileSizeMegaBytes (MiB). Za katalog i manifest dovoljna je bilo koja primjenjiva ovlast Upload, ViewReports, DownloadOutputs ili ManageSubfeeds.

ListScheduledPeriodsAsync i odgovarajuće metode prema ključu subfeeda ili partnera vraćaju poslovne oznake, ključeve razdoblja, početak, kraj, rokove, dodatno vrijeme i scheduleRevision. Odaberite vraćeno razdoblje; klijent ne računa rok. Neobavezni from i to navode se zajedno kao datumi: početak razdoblja uključuje from, a isključuje to, u vremenskoj zoni feeda. Bez filtara katalog sadrži 11 prošlih razdoblja, trenutačno razdoblje i 1 buduće. Zahtjev smije obuhvatiti najviše 60 prošlih i 24 buduća razdoblja; prevelik raspon odbija se bez skraćivanja. Ad hoc feedovi vraćaju prazan katalog i zadržavaju postojeći prijenos.

Katalog razdoblja ne sadrži povijest predaja ni podatke validacije. UploadAllowed i UploadBlockedReason odražavaju trenutačne ovlasti, životni ciklus, razdoblja aktivnosti i pravila organizacije. Stvaranje predaje ponovno provjerava postavke i ograničenja datoteka. Pozovite CreateUploadSessionByPeriodAsync ili odgovarajuću metodu subfeeda uz vraćeni ključ razdoblja, scheduleRevision, sve datoteke i opcije prijenosa. Prenesite datoteke na vraćena odredišta pa pozovite metodu dovršetka.

Odabrano razdoblje i scheduleRevision spremite uz verziju izvještaja i Idempotency-Key. Nova predaja sa starom revizijom vraća HTTP 409 PeriodSelectionStale: osvježite katalog i ponovno odaberite. Odgovarajući postojeći ključ najprije vraća prvotnu predaju, čak i nakon promjene rasporeda, uz njezino spremljeno razdoblje i rok. Promjena odabira uz isti ključ vraća IdempotencyKeyConflict. Pri oporavku postojećeg prijenosa nemojte prešutno osvježavati odabir.

GetSubmissionAsync(submissionId) koristi samo ID dobiven pri prijenosu ili u webhooku i zahtijeva ViewReports. Vraća spremljeno razdoblje i rok, stvarnog partnera predaje, sažete statuse, podatke o datotekama, prihvaćenost i CurrentAcceptedSubmissionId. Ne izdaje poveznice za preuzimanje ni pojedinosti pogrešaka validacije. Detaljnu validaciju pregledajte u sučelju. Neuspjela ispravka ne zamjenjuje raniju prihvaćenu predaju.

GetOriginalDownloadAsync(submissionId, submittedFileId) zahtijeva DownloadOutputs i izdaje privremenu poveznicu za točnu zadržanu predanu datoteku, uz zapis aktivnosti. To uključuje neuspjele, odbijene, arhivirane i zamijenjene predaje. DownloadOriginalFileAsync preuzima datoteku. Isteklu poveznicu osvježite istim ID-ovima; nedostajuće ili izbrisane datoteke vraćaju 404 i nikad se ne zamjenjuju drugom predajom. Metode prema roku odabiru trenutačnu prihvaćenu predaju. Status zajedničkog razdoblja zadržava podatke posljednjeg pokušaja, a AcceptedFilesAvailable opisuje trenutačnu prihvaćenu predaju.

AutomationSubfeedIdentity opisuje subfeed poljima SubfeedId, SubfeedKey i SubfeedStatus. Subfeed može imati više primatelja ili nijednog. Polja AutomationSubmission.PartnerId i PartnerKey označavaju partnera kojem je predaja pripisana i mogu biti null za predaje organizacije. Partnerov pristup ovisi o trenutačnim ovlastima i dodjelama.

Primjer koda

Pronađite → odaberite → prenesite → dohvatite istu predaju

Ovaj .NET primjer izvodi jedan pokušaj sa svim ulaznim datotekama u jednoj predaji. Ključ razdoblja bira se iz kataloga. Integracija sprema odabir, ključ, sadržaj datoteka, ID predaje i dovršeni korak za oporavak. Pri pokretanju primjera slijedite upute za ponavljanje zahtjeva i paralelne prijenose.

DiscoveryExample.cs
using DtoFlow.Integration;
using DtoFlow.Integration.Models;

namespace DtoFlow.IntegrationExample;

public static class DiscoveryExample
{
    // periodKey is a choice from the returned catalog, for example a selection saved by your UI.
    // Keep the reportVersionKey, selection and file bytes for retries of this report version.
    public static async Task<AutomationSubmission> RunAsync(
        DtoFlowAutomationClient client, string feedSlug, string subfeedKey, string periodKey,
        string reportVersionKey, IReadOnlyList<(string Path, string ContentType)> inputs,
        string downloadDirectory, CancellationToken cancellationToken)
    {
        var connection = await client.GetConnectionAsync(cancellationToken);
        if (!connection.Capabilities.Contains("PeriodSelection"))
            throw new InvalidOperationException("This connection does not support period selection.");
        AutomationAccessibleFeed? feed = null;
        for (var page = 1; ; page++)
        {
            var feeds = await client.ListAccessibleFeedsAsync(page, cancellationToken);
            feed = feeds.Items.SingleOrDefault(item => item.Slug == feedSlug);
            if (feed is not null || !feeds.HasNextPage) break;
        }
        if (feed is null) throw new InvalidOperationException("The token cannot access the configured feed.");

        AutomationSubfeedResourceResponse? destination = null;
        for (var page = 1; ; page++)
        {
            var subfeeds = await client.ListSubfeedsAsync(feed.Slug, page, cancellationToken);
            destination = subfeeds.Items.SingleOrDefault(item => item.SubfeedKey == subfeedKey);
            if (destination is not null || !subfeeds.HasNextPage) break;
        }
        if (destination is null) throw new InvalidOperationException("The upload destination is unavailable.");
        var manifest = await client.GetSubfeedManifestBySubfeedKeyAsync(feed.Slug, destination.SubfeedKey, cancellationToken);
        var periods = await client.ListSubfeedScheduledPeriodsBySubfeedKeyAsync(feed.Slug, destination.SubfeedKey,
            cancellationToken: cancellationToken);
        var period = periods.Single(item => item.PeriodKey == periodKey);
        if (!period.UploadAllowed) throw new InvalidOperationException(period.UploadBlockedReason);
        if (inputs.Count > manifest.Feed.UploadLimits.MaxFileCount)
            throw new InvalidOperationException("Too many files for one submission.");
        var selection = new AutomationPeriodSelection(period.PeriodKey, period.ScheduleRevision);
        var files = inputs.Select(input => new CreateAutomationUploadFileRequest(null,
            System.IO.Path.GetFileName(input.Path), input.ContentType, new FileInfo(input.Path).Length)).ToArray();
        var session = await client.CreateSubfeedUploadSessionByPeriodAndSubfeedKeyAsync(feed.Slug, destination.SubfeedKey,
            selection, files, new AutomationUploadRequestOptions { IdempotencyKey = reportVersionKey }, cancellationToken);
        // Save session.SubmissionId before uploading. A recovered session contains only missing targets.
        foreach (var target in session.Files)
        {
            var input = inputs.Single(input => System.IO.Path.GetFileName(input.Path) == target.OriginalFileName);
            await using var source = File.OpenRead(input.Path);
            await client.UploadFileAsync(target, source, cancellationToken);
        }
        await client.CompleteSubfeedUploadBySubfeedKeyAsync(feed.Slug, destination.SubfeedKey, session.SubmissionId, cancellationToken);
        AutomationSubmission submission;
        for (var attempt = 0; ; attempt++)
        {
            submission = await client.GetSubmissionAsync(session.SubmissionId, cancellationToken);
            if (submission.Accepted || submission.Status is "ValidationFailed" or "Rejected" or "Archived") break;
            if (attempt == 29) throw new TimeoutException($"Check submission {session.SubmissionId} again later.");
            await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
        }
        Directory.CreateDirectory(downloadDirectory);
        foreach (var file in submission.Files)
        {
            var link = await client.GetOriginalDownloadAsync(submission.SubmissionId, file.SubmittedFileId, cancellationToken);
            // A local name based on the file ID avoids trusting remote names as filesystem paths.
            var destinationPath = System.IO.Path.Combine(downloadDirectory, $"{file.SubmittedFileId:N}.original");
            await SaveDownloadAsync(destinationPath,
                (output, ct) => client.DownloadOriginalFileAsync(link, output, ct), cancellationToken);
        }
        return submission; // Inspect validation details in the GUI when Status is ValidationFailed.
    }

    static async Task SaveDownloadAsync(string destinationPath,
        Func<Stream, CancellationToken, Task> download, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(download);
        cancellationToken.ThrowIfCancellationRequested();
        var fullPath = Path.GetFullPath(destinationPath);
        if (File.Exists(fullPath)) throw new IOException("The download destination already exists.");
        var temporaryPath = Path.Combine(Path.GetDirectoryName(fullPath)!, $".dtoflow-{Guid.NewGuid():N}.partial");
        var output = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
        try
        {
            await using (output)
            {
                await download(output, cancellationToken);
            }
            cancellationToken.ThrowIfCancellationRequested();
            File.Move(temporaryPath, fullPath); // Same directory; never replace an existing destination.
        }
        finally
        {
            File.Delete(temporaryPath); // Only this attempt's file; a completed move leaves nothing here.
        }
    }
}

Nastavak mjesečnog prijenosa izvještaja

Pri prijenosu više izvještaja istodobno provodite najviše četiri prijenosa, od stvaranja sesije do dovršetka. Sesiju stvorite tek kada je dostupno mjesto za prijenos, čitajte datoteku postupno i spremite rezultat prije nastavka. Najprije provjerite razdoblje i preskočite Subfeedove koji već imaju prihvaćenu predaju.

Poslužitelj prema zadanim postavkama dopušta osam istodobnih API operacija prijenosa po organizaciji, 32 po instanci API-ja te jedan zahtjev za stvaranje sesije u sekundi uz početni kapacitet od osam zahtjeva. Dovršetak ne troši kapacitet stvaranja sesija. Odgovor HTTP 429 s kodom AutomationUploadThrottled uključuje Retry-After: zaustavite pokretanje novih prijenosa u cijeloj organizaciji barem do isteka tog vremena.

Za jednu verziju izvještaja možete poslati Idempotency-Key: jednu nepraznu vrijednost od najviše 128 ispisivih ASCII znakova. Pri ponavljanju zadržite velika i mala slova, razmake, metapodatke zahtjeva i isti sadržaj datoteke. Isti ključ vraća istu predaju unutar feeda i Subfeeda, čak i nakon zamjene pristupnog tokena. Promijenjeni metapodaci vraćaju 409 IdempotencyKeyConflict. Namjerni ispravak zahtijeva novi ključ nakon rješavanja nedovršene sesije. Jamstvo vrijedi dok predaja postoji.

Za prepoznavanje istih predaja uz različite ključeve pokretanja pošaljite contentSha256 (64 heksadekadska znaka SHA-256) za svaku datoteku zajedno s Idempotency-Key. Uspoređuje se cijeli skup datoteka, uključujući nazive i definicije datoteka feeda, unutar iste organizacije, feeda, Subfeeda i zakazanog razdoblja; u zajedničkim feedovima partneri se također promatraju zasebno. Isti skup vraća postojeću predaju čak i tijekom obrade ili nakon neuspjele validacije. Dodavanje, uklanjanje ili promjena datoteke stvara drugi skup; drugi Subfeed ili razdoblje ostaje neovisan. Datoteke se prenose izravno iz vaše integracije u pohranu blobova. DTO Flow koristi kontrolni zbroj koji navede klijent za prepoznavanje ponovljenih zahtjeva, a pri dovršetku provjerava svojstva bloba; ne preuzima sadržaj radi provjere kontrolnog zbroja. Izračunajte ga iz namjeravane datoteke čiji se sadržaj neće mijenjati; to nije poslužiteljska potvrda integriteta. Predaje bez deklariranih kontrolnih zbrojeva ne uspoređuju se. Za namjernu ponovnu validaciju istog sadržaja pošaljite allowDuplicateContent: true s novim ključem nakon rješavanja nedovršene sesije.

Vaša integracija pohranjuje napredak. Spremite identitet feeda i Subfeeda, rok, verziju datoteke ili lokalni kontrolni zbroj, ključ, ID predaje, dovršeni korak, broj pokušaja, ishod, kod pogreške i vrijeme sljedećeg pokušaja. Ključ spremite prije stvaranja sesije, a napredak nakon svakog koraka. U evidenciju napretka nikada ne spremajte privatne tokene ni potpisane URL-ove pohrane.

Nakon prekida provjerite postoji li prihvaćena predaja, a zatim ponovite zahtjev s istim ključem za nedovršeni prijenos. Samo datoteke koje nedostaju dobivaju nove URL-ove za prijenos, valjane 15 minuta. Ako je odgovor na PUT izgubljen, ponovno dohvatite sesiju prije novog PUT zahtjeva; nemojte prepisivati datoteke niti pogrešku pohrane smatrati dokazom primitka. Nepodudaranje veličine zahtijeva provjeru. Nakon neizvjesnog odgovora na dovršetak ponovite dovršetak za spremljeni ID predaje. Ponovljeni zahtjev za dovršenu sesiju ne vraća URL-ove za prijenos.

Dopustite najviše pet pokušaja po koraku, uključujući prvi. Počnite s odgodom od dvije sekunde, udvostručujte je do osnovne odgode od 60 sekundi, dodajte slučajnih 0–1 sekundu i poštujte dulju odgodu Retry-After. Mrežne pogreške, HTTP 408/429 i privremene pogreške 500/502/503/504 ponavljajte uz odgovarajući postupak oporavka. Nakon iscrpljenih pokušaja zadržite ishod RetryPending. .NET SDK izlaže RetryAfter. SDK pozivi po zadanim postavkama imaju jedan pokušaj; uključite AutomationRateLimitRetryPolicy za automatsko čekanje kod potvrđenog privremenog ograničenja prijenosa. Dijelite isti objekt među radnicima jedne organizacije, obuhvatite jedan API poziv i uračunajte pokušaje u istih pet pokušaja po koraku. Ostale pogreške zahtijevaju prethodno opisani oporavak.

Za svaki Subfeed zabilježite Uploaded, AlreadyAccepted, Processing, ValidationFailed, SourceMissing, RetryPending, ActionRequired ili Cancelled. Dovršetak ne znači nužno prihvaćanje: validaciju koja još traje označite kao Processing, a neuspjelu kao ValidationFailed; status razdoblja pokazuje trenutačno prihvaćenu predaju. Zadržite uspješne rezultate kada drugi Subfeed ne uspije. Kod pogrešaka autentifikacije, pristupa organizaciji ili kvote zaustavite pokretanje novih prijenosa za organizaciju; kod pogreške pojedinog Subfeeda nastavite ostale. Za kvote provjerite Error.LimitKey. Otkazivanje zadržava posljednji spremljeni napredak.

Postavite AutomationUploadRequestOptions.IdempotencyKey kako biste mogli nastaviti prekinuti prijenos. Bez ključa možete ponovno koristiti odgovarajuću otvorenu sesiju, ali neizvjestan rezultat stvaranja zahtijeva provjeru. Vaša integracija upravlja mjesečnim postupkom i spremljenim napretkom; svaki izvještaj koristi zasebnu sesiju prijenosa. API ograničenja ne rezerviraju trajna mjesta za nedovršene sesije.

Datoteke

API-ji za prijenos i preuzimanje

API i predaje kroz portal koriste ista pravila datoteka i povijest. Rute zajedničkog feeda odnose se na zajedničku razmjenu. Za podijeljeni feed odaberite Subfeed stabilnim ključem ili trenutačnom dodjelom partnera prije provjere razdoblja, prijenosa ili preuzimanja prihvaćenih datoteka. Osnovni primjeri zajedničkog feeda u nastavku nisu rute Subfeedova.

  1. Pročitajte manifest feeda kako biste potvrdili očekivane datoteke, ekstenzije, ograničenja prijenosa i opsege.
  2. Izradite sesiju prijenosa za razdoblje određeno rokom i navedite svaku datoteku u paketu.
  3. Prenesite svaku datoteku na njezin izravni URL za prijenos prije isteka URL-a.
  4. Dovršite predaju kako bi DTO Flow mogao uskladiti datoteke, validirati ih i ažurirati razdoblje.
  5. Provjerite razdoblje prema roku prije pokretanja uvoza u odredišni sustav ili posla preuzimanja.
  6. Zatražite poveznice prihvaćenih datoteka i preuzmite svaku datoteku prije isteka URL-ova za preuzimanje.

Osnovni API URL: https://api.dtoflow.com. Za zahtjeve prema API-ju za automatizaciju šaljite Authorization: Bearer <private-access-token>. OpenAPI ugovor dostupan je na https://api.dtoflow.com/api/automation/v1/openapi.json.

Ako se automatizacija povezuje preko prilagođene API adrese, koristite isti osnovni URL za OpenAPI dokument.

Partneri

Automatizacija feeda podijeljenog prema partneru

Feed podijeljen prema partneru prati neovisne Subfeedove, od kojih svaki može imati više primatelja Partnera. Prijenose, status razdoblja i prihvaćene datoteke odaberite stabilnim ključem Subfeeda. Odabir prema ključu Partnera razrješava njegovu trenutačnu dodjelu; Partner nije isključivi vlasnik Subfeeda.

  • AutomationSubfeedIdentity opisuje subfeed poljima SubfeedId, SubfeedKey i SubfeedStatus. Subfeed može imati više primatelja ili nijednog. Polja AutomationSubmission.PartnerId i PartnerKey označavaju partnera kojem je predaja pripisana i mogu biti null za predaje organizacije. Partnerov pristup ovisi o trenutačnim ovlastima i dodjelama.
  • Skupni zahtjevi za manifeste, statuse razdoblja, Partnere i Subfeedove prihvaćaju uređene korelacijske identifikatore i vraćaju neovisan rezultat za svaku stavku.
  • Operativna dopuštenja ostaju odvojena od dopuštenja ManageSubfeeds na razini feeda te ManagePartners i InvitePartners na razini organizacije. Spremnost i dodjela zahtijevaju ManageSubfeeds i ManagePartners; pozivanje zahtijeva InvitePartners i postojeću dodjelu partnera feedu iz rute.
  • Za zajedničke feedove koristite metode na razini feeda. Za feedove podijeljene prema partneru koristite metode subfeeda i odaberite odredište prema ključu subfeeda ili dodijeljenog partnera.
GetSubfeedManifestBySubfeedKeyAsync(...) i GetSubfeedManifestByPartnerKeyAsync(...)
Razrješava točan Subfeed i vraća naslijeđenu definiciju nadređenog feeda bez dupliciranja konfiguracije na Subfeedu.
GetSubfeedPeriodStatusBySubfeedKeyAsync(...) i GetSubfeedPeriodStatusByPartnerKeyAsync(...)
Čita spremnost, validaciju, trenutačno prihvaćenu predaju i dostupnost prihvaćenih datoteka za odabrani rok Subfeeda.
CreateSubfeedUploadSessionBySubfeedKeyAsync(...) i CreateSubfeedUploadSessionByPartnerKeyAsync(...)
CompleteSubfeedUploadBySubfeedKeyAsync(...) i CompleteSubfeedUploadByPartnerKeyAsync(...)
Stvara i dovršava prijenos u odabranom opsegu Subfeeda. Dodjela primatelja ostaje odvojena od neobvezne atribucije Partnera na predaji.
GetSubfeedAcceptedFilesBySubfeedKeyAsync(...) i GetSubfeedAcceptedFilesByPartnerKeyAsync(...)
Vraća kratkotrajne poveznice za preuzimanje samo prihvaćenih datoteka u odabranom razdoblju Subfeeda.
GetSubfeedManifestBatchAsync(...) i GetSubfeedPeriodStatusesBatchAsync(...)
Razrješava kombinirane odabire prema ključu Subfeeda i ključu Partnera redoslijedom zahtjeva, uz pogreške na razini stavke za nevaljane odabire.
UpsertPartnerAsync(...) i UpsertPartnersBatchAsync(...)
Stvara ili ažurira kanonske Partnere organizacije pojedinačno ili skupno. Samo to ne daje pristup feedu niti dodjeljuje primatelja Subfeedu.
UpsertSubfeedByPartnerKeyAsync(...) i UpsertSubfeedsBatchAsync(...)
Idempotentno stvara ili ažurira Subfeedove unutar feeda. Za neovisan opseg koristite PartnerKey null, pa zasebno dodijelite primatelje. Stavka Rejected ne poništava nepovezane rezultate.
ActivateSubfeedByPartnerKeyAsync(...), ActivateSubfeedsBatchAsync(...), ArchiveSubfeedByPartnerKeyAsync(...), i ArchiveSubfeedsBatchAsync(...)
Aktivira ili arhivira Subfeedove pojedinačno prema ključu Partnera ili u paketima s neovisnim rezultatima.
CheckPartnerReadinessBatchAsync(...)
Čita identitet Partnera, pristup feedu, namjeravani Subfeed, dodjelu i stanje pozivnice uz RequiredActions, SubfeedUploadReady i PortalReady. Ne mijenja zapise niti traži slanje e-pošte.
AssignPartnersToFeedAsync(...), AssignPartnersToSubfeedsBatchAsync(...), UnassignPartnersFromSubfeedsBatchAsync(...)
Pristup feedu dodjeljuje ulogu; dodjela Subfeedu odabire opseg izvještaja primatelja. Uklanjanje dodjele ukida pristup tom opsegu i njegovoj povijesti. To su izričiti upisi, odvojeni od dodavanja i ažuriranja kataloga.
InvitePartnersAsync(...)
Stvara ili ponovno koristi pozivnice za dodijeljene primatelje. Odaberite ReturnLink za svoju sigurnu dostavu ili Email za dostavu e-poštom iz DTO Flowa; provjerite rezultat svake stavke.
Primjer koda

Provjerite spremnost primatelja prije promjena

Ovaj primjer samo čita stanje: opisuje jednog namjeravanog primatelja i ispisuje preostale radnje. Ne stvara pristup, ne dodjeljuje primatelje i ne šalje pozivnice. Podržane radnje primijenite zasebno prema redoslijedu u nastavku.

check-recipients.cs
using DtoFlow.Integration;
using DtoFlow.Integration.Models;

var baseUrl = Environment.GetEnvironmentVariable("DTOFLOW_BASE_URL") ?? "https://api.dtoflow.com";
var token = Environment.GetEnvironmentVariable("DTOFLOW_TOKEN")
    ?? throw new InvalidOperationException("Set DTOFLOW_TOKEN.");
var feedSlug = Environment.GetEnvironmentVariable("DTOFLOW_FEED_SLUG") ?? "supplier-prices";
var partnerKey = Environment.GetEnvironmentVariable("DTOFLOW_PARTNER_KEY") ?? "northwind";
var subfeedKey = Environment.GetEnvironmentVariable("DTOFLOW_SUBFEED_KEY") ?? "northwind-prices";

using var httpClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false, UseDefaultCredentials = false }) { BaseAddress = new Uri(baseUrl) };
var client = new DtoFlowAutomationClient(httpClient, token);

var readiness = await client.CheckPartnerReadinessBatchAsync(feedSlug, new[]
{
    new AutomationPartnerReadinessItem(
        "recipient-1", partnerKey, "Northwind Foods", "recipient@example.invalid",
        "PartnerObserver", subfeedKey, "Active")
});

foreach (var result in readiness.Results)
{
    Console.WriteLine($"{result.CorrelationId}: {result.Outcome} {result.Error?.Code}");
    Console.WriteLine($"Upload ready: {result.SubfeedUploadReady}; portal ready: {result.PortalReady}");
    Console.WriteLine(string.Join(", ", result.RequiredActions));
}

Upravljanje ponavljajućom integracijom Subfeedova

Odvojite postavljanje primatelja od objave izvještaja. Odgovor o spremnosti koristite za planiranje promjena, a svaki Subfeed i razdoblje pratite zasebno.

1. Provjerite postavke prije promjena primatelja

U DTO Flowu postavite smjer feeda, praćenje podijeljeno prema partneru, očekivane definicije datoteka, raspored i dopuštenja tokena. Prije upisa pročitajte manifest i provjerite pravila i ograničenja datoteka. Uspješno čitanje manifesta ne potvrđuje dopuštenja za upravljanje ni spremnost primatelja.

Postavke integracije čuvajte u svojoj aplikaciji. Prije prvog prijenosa provjerite dostupne feedove, manifest i odabrano razdoblje pozivima samo za čitanje. Integracije s više datoteka moraju poslati cijeli obvezni skup. PDF i druge datoteke bez tabličnih provjera arhiviraju se neizmijenjene.

2. Odredite tko upravlja identitetima i pristupom

Partner predstavlja primatelja; Subfeed je neovisan opseg izvještaja koji dijele njegovi dodijeljeni primatelji. Koristite stabilne poslovne ključeve. Stvaranje Partnera, dodjela uloge u feedu, stvaranje Subfeeda i dodjela Partnera Subfeedu zasebne su operacije. Partner ima jednu trenutačnu dodjelu Subfeedu unutar feeda podijeljenog prema partneru.

Dodavanje ili ažuriranje mijenja samo poslane zapise. Izostavljeni primatelji ne gube dodjelu, a izostavljeni Subfeedovi ne arhiviraju se. Pristup ukinite izričito: uklonite dodjelu primatelja prije pristupa feedu, a arhivirajte tek kada ukidate cijeli opseg. Dodjela omogućuje pristup postojećoj povijesti Subfeeda; njezino uklanjanje ukida taj pristup. Odredite kojim poljima upravlja vaš sustav, a kojima administratori portala, kako usklađivanje ne bi poništilo namjerne izmjene.

3. Primijenite potrebne radnje redoslijedom ovisnosti

CheckPartnerReadinessBatchAsync samo čita stanje. Podržane RequiredActions primijenite redom: identitet Partnera, pristup i uloga u feedu, životni ciklus Subfeeda, dodjela, pa pozivnica. Zajednički Subfeed stvorite jednom uz PartnerKey null, a primatelje mu dodijelite zasebno. Nakon upisa ponovno pročitajte spremnost; za odbijene rezultate ili nepoznate radnje zatražite provjeru.

SubfeedUploadReady i PortalReady odgovaraju na različita pitanja. Prihvaćanje pozivnice i potvrda e-pošte mogu biti na čekanju dok su prijenosi organizacije već dopušteni. Sukobi identiteta ili pristupa zahtijevaju provjeru. Prije predaje u zajednički Subfeed pregledajte spremnost njegovih primatelja i primijenite pravila svoje organizacije.

4. Razlikujte stvaranje, dostavu i prihvaćanje pozivnice

InvitePartnersAsync podržava ReturnLink i Email. ReturnLink vraća tajnu poveznicu pozivnice za vašu sigurnu dostavu, bez zahtjeva za slanjem pozivnice e-poštom. Email stavlja dostavu u red čekanja. Nijedno ne potvrđuje prihvaćanje ni mogućnost prijave. ReturnLink ne zaustavlja poruke o novom pristupu feedu postojećim aktivnim računima, konfigurirane obavijesti ni prethodno zakazane poruke.

Za prijelaz s ReturnLink na Email izričito zatražite Email za pozivnicu na čekanju čiji je EmailDeliveryStatus NotRequested. Ne ponavljajte dostavu koja je u redu ili je poslana; neuspjelu ili potisnutu dostavu provjerite prije novog zahtjeva. Ako je vaša dostava poveznice izgubljena, provjerite svoju evidenciju i ponovno zatražite ReturnLink prema pravilima ponovne uporabe i isteka. AwaitInvitationAcceptance sam ne ponavlja dostavu. Tajnu poveznicu nemojte zapisivati u zapisnike; za početne testove koristite adrese .invalid, a za test prijave adrese kojima upravljate.

5. Oporavite djelomične ili neizvjesne rezultate

Pakete podijelite prema dokumentiranom ograničenju od 1.000 stavki i koristite jedinstvene korelacijske identifikatore unutar zahtjeva. Provjerite svaki rezultat, uključujući Rejected i Error, čak i kada HTTP zahtjev uspije. Neuspjeh jedne stavke ne poništava uspješne stavke. Sačuvajte vezu stavke s podatkom u svom sustavu; nedostajući rezultat nije uklanjanje ni potvrda uspjeha.

Nakon prekida zahtjeva za upravljanje ponovno pročitajte spremnost kako biste utvrdili što je uspjelo i primijenite samo preostale radnje. Oporavak prijenosa koristi spremljeni ključ idempotentnosti i nepromijenjenu datoteku. Sačuvajte dovršene opsege, privremene pogreške ponavljajte uz ograničen broj pokušaja i rastuće odgode, a probleme dopuštenja, identiteta, kvota i validacije prikažite operateru.

6. Jednom odaberite razdoblje i zadržite ga za ponavljanja

Spremite raspored feeda u DTO Flowu, a integracijske vrijednosti iz pregleda razdoblja koristite kao primjere pri postavljanju. Vaš raspoređivač iz poslovnih podataka i spremljenog rasporeda i vremenske zone izračunava rok, zatim čita status razdoblja prema roku kako bi dobio kanonski periodKey. API odabire razdoblje pomoću deadlineAt; ne zahtijeva slanje obiju vrijednosti i nema zasebnu krajnju točku /current.

Odabrano razdoblje i scheduleRevision spremite uz verziju izvještaja i Idempotency-Key. Nova predaja sa starom revizijom vraća HTTP 409 PeriodSelectionStale: osvježite katalog i ponovno odaberite. Odgovarajući postojeći ključ najprije vraća prvotnu predaju, čak i nakon promjene rasporeda, uz njezino spremljeno razdoblje i rok. Promjena odabira uz isti ključ vraća IdempotencyKeyConflict. Pri oporavku postojećeg prijenosa nemojte prešutno osvježavati odabir.

7. Razlikujte ponavljanje pokušaja od namjernog ispravka

Najprije provjerite postoji li prihvaćena predaja za odabrani Subfeed i razdoblje. U običnom izvođenju preskočite je; za namjeran ispravak koristite novu verziju datoteke ili pokušaja i novi ključ idempotentnosti. Uz contentSha256 za svaku datoteku DTO Flow može ponovno koristiti jednak potpuni skup datoteka unutar istog opsega i razdoblja. Za prepoznavanje ponavljanja koristi sažetke koje šalje pozivatelj, bez preuzimanja blobova radi ponovnog izračuna.

Processing znači da validacija nije dovršena; ValidationFailed zahtijeva provjeru. Nijedno ne treba uzrokovati ponavljano slanje jednakih datoteka. Promijenjene datoteke ili promijenjen potpuni skup mogu stvoriti novu predaju; nepromijenjene datoteke mogu se koristiti i u drugom Subfeedu ili razdoblju. Za namjernu ponovnu validaciju jednakog skupa koristite novi ključ i allowDuplicateContent: true. Najprije razriješite otvorenu sesiju.

8. Čuvajte napredak i planirajte oporavak u svom sustavu

Spremajte opseg, razdoblje i rok, nepromjenjive predane datoteke i njihovu verziju, ključ ponavljanja, ID predaje i ishod. Zadržite datoteke dostupnima nakon ponovnog pokretanja i spriječite istodobne promjene istog opsega. Uklanjanje opsega iz vašeg kataloga ne briše udaljene podatke; sačuvajte njegovu povijest oporavka.

Vaš raspoređivač određuje kada se posao sa statusom RetryPending ponovno pokreće. Poštujte Retry-After i ograničenja broja pokušaja; za status Processing naknadno provjerite predaju ili obradite webhook o dovršetku. SDK sam ne raspoređuje poslove i svaki poziv zadano izvodi jedan pokušaj. Zapisujte sigurne kodove pogrešaka i identifikatore predaja, izostavite tajne i u svojoj integraciji koristite određenu verziju SDK-a.

Ključevi ponavljanja, ograničenja i oporavak sesije prijenosa · Zajednička predaja više datoteka

Primjeri

Osnovni primjer razmjene datoteka

Počnite jednom malom, ponovljivom razmjenom: predajte očekivanu datoteku, dovršite predaju i preuzmite prihvaćenu datoteku kada je razdoblje spremno.

  • Prenesite CSV, Excel, ZIP ili druge očekivane datoteke iz planirane mape.
  • Provjerite ima li trenutačno razdoblje prihvaćenu datoteku prije pokretanja uvoza u odredišni sustav.
  • Preuzmite trenutačnu prihvaćenu predanu datoteku kada bude spremna.

.NET SDK: DtoFlow.Integration 1.0.0 na NuGetu

Stabilni SDK podržava otkrivanje dostupnih feedova, odabir razdoblja, prijenos i preuzimanje datoteka te provjeru webhook potpisa. Namijenjen je platformi .NET Standard 2.0 i novijim kompatibilnim platformama.

Primjer koda

Instalirajte paket

.NET CLI
dotnet add package DtoFlow.Integration --version 1.0.0
Primjer koda

Predajte datoteku, dovršite predaju i preuzmite prihvaćene datoteke

Koristite varijable okruženja za tajne i postavke feeda specifične za instalaciju. .NET primjeri spremaju datoteke pod konačnim nazivom tek nakon uspješnog preuzimanja, bez zamjene postojećih datoteka.

Program.cs
using System.Globalization;
using DtoFlow.Integration;
using DtoFlow.Integration.Models;

var baseUrl = Environment.GetEnvironmentVariable("DTOFLOW_BASE_URL") ?? "https://api.dtoflow.com";
var token = Environment.GetEnvironmentVariable("DTOFLOW_TOKEN")
    ?? throw new InvalidOperationException("Set DTOFLOW_TOKEN.");
var feedSlug = Environment.GetEnvironmentVariable("DTOFLOW_FEED_SLUG") ?? "supplier-prices";
var filePath = args.Length > 0 ? args[0] : "prices.csv";
var deadlineAt = DateTimeOffset.Parse(
    Environment.GetEnvironmentVariable("DTOFLOW_DEADLINE_AT") ?? "2026-06-01T12:00:00Z",
    CultureInfo.InvariantCulture);

using var httpClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false, UseDefaultCredentials = false }) { BaseAddress = new Uri(baseUrl) };
var client = new DtoFlowAutomationClient(httpClient, token);
var fileInfo = new FileInfo(filePath);

try
{
    var session = await client.CreateUploadSessionAsync(
        feedSlug,
        deadlineAt,
        new[]
        {
            new CreateAutomationUploadFileRequest(
                FeedFileSlug: null,
                OriginalFileName: fileInfo.Name,
                ContentType: "text/csv",
                SizeBytes: fileInfo.Length)
        });

    using var upload = File.OpenRead(filePath);
    await client.UploadFileAsync(session.Files[0], upload);
    await client.CompleteUploadAsync(feedSlug, session.SubmissionId);

    AutomationPeriodStatus? period = null;
    for (var attempt = 0; attempt < 30; attempt++)
    {
        period = await client.GetPeriodStatusByDeadlineAsync(feedSlug, deadlineAt);
        if (period.AcceptedFilesAvailable)
        {
            break;
        }

        if (period.Status is "ValidationFailed" or "Rejected" or "Archived")
        {
            throw new InvalidOperationException(
                $"Submission {period.SubmissionId} finished with status {period.Status}.");
        }

        await Task.Delay(TimeSpan.FromSeconds(2));
    }

    if (period?.AcceptedFilesAvailable != true)
    {
        throw new TimeoutException("The accepted file was not ready within one minute.");
    }

    var accepted = await client.GetAcceptedFilesByDeadlineAsync(feedSlug, deadlineAt);
    foreach (var acceptedFile in accepted.Files)
    {
        var destinationPath = ResolveDownloadPath(
            Directory.GetCurrentDirectory(),
            acceptedFile.SubmittedFileId);
        await SaveDownloadAsync(destinationPath,
            (destination, ct) => client.DownloadAcceptedFileAsync(acceptedFile, destination, ct));
        Console.WriteLine($"Downloaded {destinationPath}");
    }
}
catch (DtoFlowAutomationException ex) when (ex.IsTenantAccessReadOnly)
{
    Console.Error.WriteLine($"Tenant access is {ex.TenantAccessState}; required action: {ex.TenantAccessRequiredAction}.");
    Environment.ExitCode = 2;
}

static string ResolveDownloadPath(string downloadDirectory, Guid submittedFileId)
{
    // Use the file identity, never an untrusted remote filename, as the local name.
    return Path.Combine(Path.GetFullPath(downloadDirectory), $"{submittedFileId:N}.original");
}

static async Task SaveDownloadAsync(string destinationPath,
    Func<Stream, CancellationToken, Task> download, CancellationToken cancellationToken = default)
{
    ArgumentNullException.ThrowIfNull(download);
    cancellationToken.ThrowIfCancellationRequested();
    var fullPath = Path.GetFullPath(destinationPath);
    if (File.Exists(fullPath)) throw new IOException("The download destination already exists.");
    var temporaryPath = Path.Combine(Path.GetDirectoryName(fullPath)!, $".dtoflow-{Guid.NewGuid():N}.partial");
    var output = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
    try
    {
        await using (output)
        {
            await download(output, cancellationToken);
        }
        cancellationToken.ThrowIfCancellationRequested();
        File.Move(temporaryPath, fullPath); // Same directory; never replace an existing destination.
    }
    finally
    {
        File.Delete(temporaryPath); // Only this attempt's file; a completed move leaves nothing here.
    }
}
Referenca

API metode i .NET referenca

Metode u nastavku dostupne su u paketu DtoFlow.Integration 1.0.0. Integracije na drugim platformama mogu koristiti izravne HTTP pozive ili objavljeni OpenAPI ugovor.

GetManifestAsync(fileFeedSlug)
Čita smjer feeda, status, ograničenja prijenosa, očekivane datoteke, pravila podudaranja i ovlasti tokena kako bi svako pokretanje moglo potvrditi aktualne postavke.
GetPeriodStatusByDeadlineAsync(fileFeedSlug, deadlineAt)
Provjerava ima li očekivano razdoblje predaju, je li validacija završila i jesu li prihvaćene datoteke dostupne za preuzimanje.
CreateUploadSessionAsync(...) i CompleteUploadAsync(...)
Izrađuje izravna odredišta za prijenos, zatim označava predaju spremnom za podudaranje i validaciju nakon prijenosa svih datoteka.
GetAcceptedFilesByDeadlineAsync(...) i DownloadAcceptedFileAsync(...)
Dohvaća kratkotrajne izravne URL-ove za preuzimanje trenutačne prihvaćene predaje i sprema svaku prihvaćenu datoteku u lokalno odredište.
Primjer koda

Pročitajte manifest i provjerite spremnost razdoblja

Koristite ovo prije pokretanja uvoza u odredišni sustav ili posla preuzimanja.

check-period.cs
using System.Globalization;
using System.Linq;
using DtoFlow.Integration;

var baseUrl = Environment.GetEnvironmentVariable("DTOFLOW_BASE_URL") ?? "https://api.dtoflow.com";
var token = Environment.GetEnvironmentVariable("DTOFLOW_TOKEN")
    ?? throw new InvalidOperationException("Set DTOFLOW_TOKEN.");
var feedSlug = Environment.GetEnvironmentVariable("DTOFLOW_FEED_SLUG") ?? "supplier-prices";
var deadlineAt = DateTimeOffset.Parse(
    Environment.GetEnvironmentVariable("DTOFLOW_DEADLINE_AT") ?? "2026-06-01T12:00:00Z",
    CultureInfo.InvariantCulture);

using var httpClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false, UseDefaultCredentials = false }) { BaseAddress = new Uri(baseUrl) };
var client = new DtoFlowAutomationClient(httpClient, token);

var manifest = await client.GetManifestAsync(feedSlug);
Console.WriteLine($"Feed: {manifest.Name} ({manifest.Slug})");
Console.WriteLine($"Direction: {manifest.Direction}; status: {manifest.Status}");
Console.WriteLine($"Token scopes: {string.Join(", ", manifest.TokenScopes)}");

foreach (var file in manifest.Files.OrderBy(file => file.SortOrder))
{
    Console.WriteLine(
        $"- {file.Slug}: {file.ExpectedExtension}, required={file.RequiredInSubmission}, validation={file.IsValidationEnabled}");
}

var period = await client.GetPeriodStatusByDeadlineAsync(feedSlug, deadlineAt);
if (!period.HasSubmission)
{
    Console.WriteLine("No submission has been completed for this period yet.");
    return;
}

if (!period.Accepted || !period.AcceptedFilesAvailable)
{
    Console.WriteLine($"Submission {period.SubmissionId} is {period.Status}; validation is {period.ValidationStatus}.");
    return;
}

var accepted = await client.GetAcceptedFilesByDeadlineAsync(feedSlug, deadlineAt);
Console.WriteLine($"{accepted.Files.Count} accepted file(s) are ready from {accepted.SubmissionId}.");
Paketi

Prijenos i preuzimanje više datoteka

Feedovi s više datoteka najbolje rade kada svaka očekivana datoteka ima slug datoteke feeda. U zahtjev za sesiju prijenosa uključite svaku datoteku, prenesite je na odredište koje API vrati, jednom dovršite predaju i zatim preuzmite sve prihvaćene datoteke za razdoblje kada DTO Flow prihvati predaju.

  • Koristite slugove datoteka feeda za strogo očekivane datoteke, poput prices ili inventory.
  • Koristite FeedFileSlug: null kada DTO Flow treba povezati jednu očekivanu datoteku prema nazivu ili za dopuštenu dodatnu datoteku. U poslovima s više datoteka koristite izričite slugove kako biste izbjegli nejasnoće.
  • Dovršite predaju nakon završetka svih prijenosa, ne nakon svake pojedinačne datoteke.
  • URL-ovi za preuzimanje vrijede kratko, zato za svako pokretanje zatražite svježe poveznice prihvaćenih datoteka.
Primjer koda

Prenesite i preuzmite razdoblje feeda s više datoteka

Isti obrazac radi za dvije datoteke ili veći skup očekivanih datoteka.

multi-file-sync.cs
using System.Globalization;
using System.Linq;
using DtoFlow.Integration;
using DtoFlow.Integration.Models;

var baseUrl = Environment.GetEnvironmentVariable("DTOFLOW_BASE_URL") ?? "https://api.dtoflow.com";
var token = Environment.GetEnvironmentVariable("DTOFLOW_TOKEN")
    ?? throw new InvalidOperationException("Set DTOFLOW_TOKEN.");
var feedSlug = Environment.GetEnvironmentVariable("DTOFLOW_FEED_SLUG") ?? "weekly-pack";
var deadlineAt = DateTimeOffset.Parse(
    Environment.GetEnvironmentVariable("DTOFLOW_DEADLINE_AT") ?? "2026-06-01T12:00:00Z",
    CultureInfo.InvariantCulture);
var downloadDirectory = Environment.GetEnvironmentVariable("DTOFLOW_DOWNLOAD_DIR") ?? "accepted";

var uploadFiles = new[]
{
    new { FeedFileSlug = "prices", Path = "incoming/prices.csv", ContentType = "text/csv" },
    new { FeedFileSlug = "inventory", Path = "incoming/inventory.xlsx", ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
};

using var httpClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false, UseDefaultCredentials = false }) { BaseAddress = new Uri(baseUrl) };
var client = new DtoFlowAutomationClient(httpClient, token);

try
{
var session = await client.CreateUploadSessionAsync(
    feedSlug,
    deadlineAt,
    uploadFiles.Select(file =>
    {
        var fileInfo = new FileInfo(file.Path);
        return new CreateAutomationUploadFileRequest(
            file.FeedFileSlug,
            fileInfo.Name,
            file.ContentType,
            fileInfo.Length);
    }).ToArray());

var uploadTargets = session.Files.ToDictionary(
    file => file.OriginalFileName,
    StringComparer.OrdinalIgnoreCase);

foreach (var uploadFile in uploadFiles)
{
    var fileName = Path.GetFileName(uploadFile.Path);
    await using var stream = File.OpenRead(uploadFile.Path);
    await client.UploadFileAsync(uploadTargets[fileName], stream);
    Console.WriteLine($"Uploaded {fileName}.");
}

await client.CompleteUploadAsync(feedSlug, session.SubmissionId);

var period = await client.GetPeriodStatusByDeadlineAsync(feedSlug, deadlineAt);
if (!period.AcceptedFilesAvailable)
{
    Console.WriteLine($"Submission {session.SubmissionId} is {period.Status}; try download after validation completes.");
    return;
}

Directory.CreateDirectory(downloadDirectory);
var accepted = await client.GetAcceptedFilesByDeadlineAsync(feedSlug, deadlineAt);

foreach (var acceptedFile in accepted.Files)
{
    var destinationPath = ResolveDownloadPath(downloadDirectory, acceptedFile.SubmittedFileId);
    await SaveDownloadAsync(destinationPath,
        (destination, ct) => client.DownloadAcceptedFileAsync(acceptedFile, destination, ct));
    Console.WriteLine($"Downloaded {destinationPath}.");
}
}
catch (DtoFlowAutomationException ex) when (ex.IsTenantAccessReadOnly)
{
    Console.Error.WriteLine($"Tenant access is {ex.TenantAccessState}; required action: {ex.TenantAccessRequiredAction}.");
    Environment.ExitCode = 2;
}

static string ResolveDownloadPath(string downloadDirectory, Guid submittedFileId)
{
    // Use the file identity, never an untrusted remote filename, as the local name.
    return Path.Combine(Path.GetFullPath(downloadDirectory), $"{submittedFileId:N}.original");
}

static async Task SaveDownloadAsync(string destinationPath,
    Func<Stream, CancellationToken, Task> download, CancellationToken cancellationToken = default)
{
    ArgumentNullException.ThrowIfNull(download);
    cancellationToken.ThrowIfCancellationRequested();
    var fullPath = Path.GetFullPath(destinationPath);
    if (File.Exists(fullPath)) throw new IOException("The download destination already exists.");
    var temporaryPath = Path.Combine(Path.GetDirectoryName(fullPath)!, $".dtoflow-{Guid.NewGuid():N}.partial");
    var output = new FileStream(temporaryPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
    try
    {
        await using (output)
        {
            await download(output, cancellationToken);
        }
        cancellationToken.ThrowIfCancellationRequested();
        File.Move(temporaryPath, fullPath); // Same directory; never replace an existing destination.
    }
    finally
    {
        File.Delete(temporaryPath); // Only this attempt's file; a completed move leaves nothing here.
    }
}