├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── QuickBooksSharp.CodeGen ├── CodeModel │ ├── ClassModel.cs │ ├── EnumModel.cs │ └── PropertyModel.cs ├── Program.cs ├── QuickBooksSharp.CodeGen.csproj └── xsd │ ├── 3.56 │ ├── Finance.xsd │ ├── IntuitBaseTypes.xsd │ ├── IntuitNamesTypes.xsd │ ├── IntuitRestServiceDef.xsd │ ├── Report.xsd │ └── SalesTax.xsd │ ├── 3.65 │ ├── Finance.xsd │ ├── IntuitBaseTypes.xsd │ ├── IntuitNamesTypes.xsd │ ├── IntuitRestServiceDef.xsd │ ├── Report.xsd │ └── SalesTax.xsd │ ├── 3.73 │ ├── Finance.xsd │ ├── IntuitBaseTypes.xsd │ ├── IntuitNamesTypes.xsd │ ├── IntuitRestServiceDef.xsd │ ├── Report.xsd │ └── SalesTax.xsd │ └── 3.75 │ ├── Finance.xsd │ ├── IntuitBaseTypes.xsd │ ├── IntuitNamesTypes.xsd │ ├── IntuitRestServiceDef.xsd │ ├── Report.xsd │ └── SalesTax.xsd ├── QuickBooksSharp.Tests ├── AuthenticationService_Tests.cs ├── DataServiceTests.cs ├── QuickBooksSharp.Tests.csproj ├── QuickBooksUrlTests.cs ├── RunPolicyTests.cs ├── ServiceTestBase.cs ├── TestHelper.cs └── WebhookEventTests.cs ├── QuickBooksSharp.sln ├── QuickBooksSharp ├── Authentication │ ├── AuthenticationService.cs │ ├── IAuthenticationService.cs │ ├── RevokeTokenRequest.cs │ ├── TokenRequest.cs │ ├── TokenResponse.cs │ └── UserInfo.cs ├── Entities │ ├── Generated.cs │ ├── IntuitResponseOfT.cs │ ├── QueryCountResponse.cs │ └── QueryResponseOfT.cs ├── Helper.cs ├── Infrastructure │ ├── IQuickBooksHttpClient.cs │ ├── NumberTimespanConverter.cs │ ├── QuickBooksException.cs │ ├── QuickBooksHttpClient.cs │ └── QuickBooksUrl.cs ├── Policies │ ├── FifoSemaphore.cs │ ├── IRunPolicy.cs │ ├── MaxConcurrencyRetryRunPolicy.cs │ ├── NoRetryRunPolicy.cs │ ├── QuickBooksAPIResponse.cs │ ├── RateLimitEvent.cs │ ├── RunPolicy.cs │ └── SimpleRetryRunPolicy.cs ├── QuickBooksSharp.csproj ├── Services │ ├── DataService.cs │ └── IDataService.cs └── Webhooks │ ├── DataChangeEvent.cs │ ├── EntityChange.cs │ ├── EntityChangedName.cs │ ├── EventNotification.cs │ └── WebhookEvent.cs └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build Test Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup .NET Core SDK 18 | uses: actions/setup-dotnet@v4 19 | with: 20 | dotnet-version: '9.0.x' 21 | - name: Install dependencies 22 | run: dotnet restore 23 | - name: Build 24 | run: dotnet build --configuration Release --no-restore 25 | - name: Test 26 | env: 27 | QUICKBOOKS_SHARP_CLIENT_ID: ${{ secrets.QUICKBOOKS_SHARP_CLIENT_ID }} 28 | QUICKBOOKS_SHARP_CLIENT_SECRET: ${{ secrets.QUICKBOOKS_SHARP_CLIENT_SECRET }} 29 | QUICKBOOKS_SHARP_OAUTH_REDIRECT_URI: ${{ secrets.QUICKBOOKS_SHARP_OAUTH_REDIRECT_URI }} 30 | QUICKBOOKS_SHARP_REFRESH_TOKEN: ${{ secrets.QUICKBOOKS_SHARP_REFRESH_TOKEN }} 31 | QUICKBOOKS_SHARP_REALMID: ${{ secrets.QUICKBOOKS_SHARP_REALMID }} 32 | run: dotnet test --no-restore --verbosity normal 33 | - name: Publish to nuget 34 | env: 35 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 36 | run: dotnet nuget push QuickBooksSharp/bin/Release/*.nupkg -s https://api.nuget.org/v3/index.json --skip-duplicate -k $NUGET_API_KEY 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 better-reports 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/CodeModel/ClassModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace QuickBooksSharp.CodeGen 4 | { 5 | public class ClassModel 6 | { 7 | public string Name { get; set; } 8 | 9 | public string BaseName { get; set; } 10 | 11 | public bool IsAbstract { get; set; } 12 | 13 | public PropertyModel[] Properties { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/CodeModel/EnumModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace QuickBooksSharp.CodeGen 4 | { 5 | public class EnumModel 6 | { 7 | public string Name { get; set; } 8 | 9 | public string[] Fields { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/CodeModel/PropertyModel.cs: -------------------------------------------------------------------------------- 1 | namespace QuickBooksSharp.CodeGen 2 | { 3 | public class PropertyModel 4 | { 5 | public string Name { get; set; } 6 | 7 | public string TypeName { get; set; } 8 | 9 | public bool IsNullable { get; set; } 10 | 11 | public bool IsArray { get; set; } 12 | 13 | public string Code { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.Formatting; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text.RegularExpressions; 9 | using System.Xml.Schema; 10 | 11 | namespace QuickBooksSharp.CodeGen 12 | { 13 | class Program 14 | { 15 | /// 16 | /// -Download latest minor version XSD from https://developer.intuit.com/app/developer/qbo/docs/develop/explore-the-quickbooks-online-api/minor-versions 17 | /// -Unzip into the xsd/3.{MinorVersion} folder 18 | /// -Update QuickBooksUrl.cs MinorVersion 19 | /// -Update README.md 20 | /// -Run program 21 | /// 22 | /// 23 | static void Main(string[] args) 24 | { 25 | string currentDir = Directory.GetCurrentDirectory(); 26 | string solutionPath = currentDir.Substring(0, currentDir.IndexOf("QuickBooksSharp")); 27 | string xsdPath = Path.Combine(solutionPath, $"QuickBooksSharp/QuickBooksSharp.CodeGen/xsd/{QuickBooksUrl.Version}"); 28 | string outFilePath = Path.Combine(solutionPath, "QuickBooksSharp/QuickBooksSharp/Entities/Generated.cs"); 29 | var schemas = Directory.GetFiles(xsdPath) 30 | .Select(filePath => XmlSchema.Read(new StringReader(File.ReadAllText(filePath)), null)) 31 | .ToArray(); 32 | var set = new XmlSchemaSet(); 33 | foreach (var s in schemas) 34 | set.Add(s); 35 | set.Compile(); 36 | 37 | var globalTypes = set.GlobalTypes.Values.Cast() 38 | .ToArray(); 39 | var elts = set.GlobalElements.Values.Cast().ToArray(); 40 | 41 | var eltNameToSubstitutedElements = elts.Where(e => !string.IsNullOrEmpty(e.SubstitutionGroup.Name)) 42 | .ToLookup(e => e.SubstitutionGroup.Name); 43 | var simpleTypes = globalTypes.OfType().ToArray(); 44 | var complexTypes = globalTypes.OfType().ToArray(); 45 | 46 | var enums = GenerateEnums(simpleTypes); 47 | var classes = GenerateClasses(complexTypes, eltNameToSubstitutedElements); 48 | GenerateOutFile(outFilePath, enums, classes); 49 | FormatOutFile(outFilePath); 50 | } 51 | 52 | private static XmlSchemaComplexType GetParentComplexType(XmlSchemaObject o) 53 | { 54 | var p = o.Parent; 55 | while (p != null && p is not XmlSchemaComplexType) 56 | p = p.Parent; 57 | 58 | return p as XmlSchemaComplexType; 59 | } 60 | 61 | private static IEnumerable GetParticlesRec(XmlSchemaComplexType complexType, XmlSchemaSequence seq) 62 | { 63 | foreach (var item in seq.Items) 64 | { 65 | var parentComplexType = GetParentComplexType(item); 66 | if (parentComplexType != null && parentComplexType != complexType) 67 | continue; 68 | switch (item) 69 | { 70 | case XmlSchemaChoice choice: 71 | yield return choice; 72 | break; 73 | 74 | case XmlSchemaElement elt: 75 | yield return elt; 76 | break; 77 | 78 | case XmlSchemaSequence innerSeq: 79 | foreach (var p in GetParticlesRec(complexType, innerSeq)) 80 | yield return p; 81 | break; 82 | 83 | case XmlSchemaAny _: 84 | //IntuitAnyType => no pties 85 | break; 86 | 87 | default: 88 | throw new Exception("Unexpected particle"); 89 | } 90 | } 91 | } 92 | 93 | private static string GetTypeName(string typeName) 94 | { 95 | return typeName switch 96 | { 97 | "id" => "string", 98 | "anyURI" => "string", 99 | "positiveInteger" => "uint", 100 | "boolean" => "bool", 101 | "date" => "DateOnly", 102 | "dateTime" => "DateTimeOffset", 103 | "anyType" => "object", 104 | _ => typeName 105 | }; 106 | } 107 | 108 | private static PropertyModel GetPropertyFromElt(XmlSchemaElement elt, bool forceIsNullable, bool forceIsArray) 109 | { 110 | return new PropertyModel 111 | { 112 | Name = elt.QualifiedName.Name ?? throw new Exception(), 113 | TypeName = GetTypeName(elt.ElementSchemaType.QualifiedName.Name), 114 | IsNullable = elt.MinOccurs == 0 || forceIsNullable, 115 | IsArray = elt.MaxOccurs == decimal.MaxValue || forceIsArray 116 | }; 117 | } 118 | 119 | private static PropertyModel GetPropertyFromAttribute(XmlSchemaAttribute attr) 120 | { 121 | return new PropertyModel 122 | { 123 | Name = attr.QualifiedName.Name ?? throw new Exception(), 124 | TypeName = GetTypeName(attr.AttributeSchemaType.QualifiedName.Name), 125 | IsNullable = attr.Use == XmlSchemaUse.Optional, 126 | IsArray = false, 127 | }; 128 | } 129 | 130 | private static ClassModel[] GenerateClasses(XmlSchemaComplexType[] complexTypes, ILookup eltNameToSubstitutedElements) 131 | { 132 | return complexTypes.Select(t => 133 | { 134 | var pties = new List(); 135 | 136 | foreach (var attr in t.Attributes.Cast()) 137 | pties.Add(GetPropertyFromAttribute(attr)); 138 | 139 | if (t.ContentTypeParticle.GetType().Name != "EmptyParticle") 140 | { 141 | var particles = GetParticlesRec(t, (XmlSchemaSequence)t.ContentTypeParticle); 142 | var elts = particles.SelectMany(p => p switch 143 | { 144 | XmlSchemaChoice choice => choice.Items.Cast().SelectMany(o => 145 | { 146 | if (o is XmlSchemaElement e) 147 | return new[] { (Elt: e, IsChoiceChild: true) }; 148 | else if (o is XmlSchemaSequence s) 149 | return GetParticlesRec(t, s).Cast().Select(e => (Elt: e, IsChoiceChild: true)); 150 | else 151 | throw new Exception(); 152 | }), 153 | XmlSchemaElement elt => new[] { (Elt: elt, IsChoiceChild: false) }, 154 | _ => throw new Exception() 155 | }); 156 | foreach (var i in elts) 157 | { 158 | if (eltNameToSubstitutedElements.Contains(i.Elt.QualifiedName.Name)) 159 | { 160 | bool isArray = i.Elt.MaxOccurs == decimal.MaxValue; 161 | bool isNullable = i.Elt.MinOccurs == 0; 162 | var subsitutionsPties = new List(); 163 | foreach (var subElt in eltNameToSubstitutedElements[i.Elt.QualifiedName.Name] 164 | .Where(i => i.QualifiedName.Name != t.Name)) 165 | { 166 | var pty = GetPropertyFromElt(subElt, true, isArray); 167 | pties.Add(pty); 168 | subsitutionsPties.Add(pty); 169 | } 170 | bool isAnyNullabe = isNullable || i.IsChoiceChild; 171 | pties.Add(new PropertyModel 172 | { 173 | Name = i.Elt.QualifiedName.Name + (isArray ? "s" : ""), 174 | TypeName = i.Elt.ElementSchemaType.QualifiedName.Name, 175 | IsArray = isArray, 176 | IsNullable = isAnyNullabe, 177 | Code = "{ get => " + string.Join(" ?? ", subsitutionsPties.Select((p, index) => (index == subsitutionsPties.Count - 1 ? $"({i.Elt.ElementSchemaType.QualifiedName.Name}{(isArray ? "[]" : "")}{(isAnyNullabe ? "?" : "")})" : "") + p.Name)) + (isAnyNullabe ? "" : "!") + "; }" 178 | }); 179 | } 180 | else 181 | { 182 | pties.Add(GetPropertyFromElt(i.Elt, i.IsChoiceChild, false)); 183 | } 184 | } 185 | } 186 | else 187 | { 188 | var contentModel = (XmlSchemaSimpleContent)t.ContentModel; 189 | var extension = (XmlSchemaSimpleContentExtension)contentModel.Content; 190 | 191 | pties.Add(new PropertyModel 192 | { 193 | Name = "value", 194 | TypeName = t.BaseXmlSchemaType.QualifiedName.Name == "id" ? "string" : t.BaseXmlSchemaType.QualifiedName.Name, 195 | IsArray = false, 196 | IsNullable = false, 197 | }); 198 | foreach (var attr in extension.Attributes.Cast()) 199 | pties.Add(GetPropertyFromAttribute(attr)); 200 | } 201 | 202 | bool isReferenceType = t.Name == "ReferenceType"; 203 | bool isCustomFieldDefinitionType = t.Name == "CustomFieldDefinition"; 204 | return new ClassModel 205 | { 206 | Name = t.Name, 207 | //todo create a custom converter that can deseriaize to the correct CustomFieldDefinition dervived type 208 | //For now, we simply mark the base type as a concrete type, since none of the dervied properties are returned by the API anyway 209 | IsAbstract = isCustomFieldDefinitionType ? false : t.IsAbstract, 210 | BaseName = isReferenceType ? null : t.BaseXmlSchemaType?.Name, 211 | Properties = pties.ToArray() 212 | 213 | }; 214 | }).Where(c => c.Name != null) 215 | .ToArray(); 216 | } 217 | 218 | private static EnumModel[] GenerateEnums(XmlSchemaSimpleType[] simpleTypes) 219 | { 220 | return simpleTypes.Select(t => new EnumModel 221 | { 222 | Name = t.Name, 223 | Fields = (t.Content as XmlSchemaSimpleTypeRestriction) 224 | .Facets 225 | .Cast() 226 | .Select(f => f.Value) 227 | .Distinct() 228 | .ToArray() 229 | }).Where(e => e.Fields.Any()) 230 | .ToArray(); 231 | } 232 | 233 | private static void GenerateOutFile(string outFilePath, EnumModel[] enums, ClassModel[] classes) 234 | { 235 | using (var writer = new StreamWriter(outFilePath)) 236 | { 237 | writer.WriteLine("using System;"); 238 | writer.WriteLine("using System.Runtime.Serialization;"); 239 | writer.WriteLine("using System.Text.Json.Serialization;"); 240 | writer.WriteLine(); 241 | writer.WriteLine("namespace QuickBooksSharp.Entities"); 242 | writer.WriteLine("{"); 243 | 244 | foreach (var e in enums) 245 | { 246 | writer.WriteLine($"public enum {e.Name}"); 247 | writer.WriteLine("{"); 248 | 249 | writer.WriteLine($"Unspecified = 0,"); 250 | foreach (var name in e.Fields) 251 | { 252 | string safeName = GetSafePropertyName(name); 253 | if (name != safeName) 254 | writer.WriteLine($"[EnumMember(Value = \"{name}\")]"); 255 | writer.WriteLine($"{safeName},"); 256 | } 257 | 258 | writer.WriteLine("}"); 259 | } 260 | 261 | foreach (var c in classes) 262 | { 263 | writer.Write($"public {(c.IsAbstract ? "abstract" : "")} class {GetSafeClassName(c.Name)}"); 264 | if (c.BaseName != null) 265 | writer.Write($" : {c.BaseName}"); 266 | writer.WriteLine(); 267 | writer.WriteLine("{"); 268 | 269 | foreach (var pty in c.Properties) 270 | { 271 | string safeName = GetSafePropertyName(pty.Name); 272 | if (pty.Name != safeName) 273 | writer.WriteLine($"[JsonPropertyName(\"{pty.Name}\")]"); 274 | 275 | if (c.Name == "BatchItemRequest") 276 | writer.WriteLine(pty.Name == "IntuitObject" ? "[JsonIgnore]" : "[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]"); 277 | 278 | static string GetPropertyDeclaration(PropertyModel pty, string safeName, string typeName) 279 | { 280 | string ptyDecl = string.Empty; 281 | ptyDecl += $"public {typeName}"; 282 | if (pty.IsArray) 283 | ptyDecl += "[]"; 284 | if (pty.IsNullable) 285 | ptyDecl += "?"; 286 | ptyDecl += $" {safeName} "; 287 | ptyDecl += pty.Code ?? "{ get; set; }"; 288 | if (!pty.IsNullable && pty.Code == null) 289 | ptyDecl += " = default!; "; 290 | 291 | return ptyDecl; 292 | } 293 | string typeName = GetSafeClassName(pty.TypeName); 294 | if (typeName != nameof(DateOnly)) 295 | writer.WriteLine(GetPropertyDeclaration(pty, safeName, typeName)); 296 | else 297 | { 298 | //DateOnly type is only support on .NET6+ 299 | writer.WriteLine("#if NET6_0_OR_GREATER"); 300 | writer.WriteLine(GetPropertyDeclaration(pty, safeName, typeName)); 301 | writer.WriteLine("#else"); 302 | writer.WriteLine(GetPropertyDeclaration(pty, safeName, "DateTime")); 303 | writer.WriteLine("#endif"); 304 | } 305 | 306 | } 307 | 308 | writer.WriteLine("}"); 309 | } 310 | 311 | writer.WriteLine("}"); 312 | } 313 | } 314 | 315 | private static void FormatOutFile(string outFilePath) 316 | { 317 | string rawText = File.ReadAllText(outFilePath); 318 | var ws = new AdhocWorkspace(); 319 | var code = CSharpSyntaxTree.ParseText(rawText); 320 | string formattedText = Formatter.Format(code.GetRoot(), ws).ToFullString(); 321 | File.WriteAllText(outFilePath, formattedText); 322 | } 323 | 324 | private static string GetSafeClassName(string name) 325 | { 326 | //Avoid name conflict with System.Threading.Tasks.Task 327 | return name == "Task" ? "QbTask" : name; 328 | } 329 | 330 | private static string GetSafePropertyName(string name) 331 | { 332 | //must escape property names that conflict with keywords 333 | if (new[] { "void" }.Contains(name)) 334 | return $"@{name}"; 335 | 336 | return Regex.Replace(name, "[-%)( ]", string.Empty); 337 | } 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/QuickBooksSharp.CodeGen.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/xsd/3.56/Report.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | Date macros enumeration 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Specifies the column type definition 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | List of all row types 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 95 | Describes the type 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Describes the Name 105 | 106 | 107 | 108 | 109 | Describes the Value 110 | 111 | 112 | 113 | 114 | 115 | 116 | List of columns 117 | 118 | 119 | 121 | 122 | Column of the report 123 | 124 | 125 | 126 | 127 | 128 | 129 | Describes a column 130 | 131 | 132 | 133 | 134 | Describes the column title name 135 | 136 | 137 | 138 | 139 | Describes the column type enumeration 140 | 141 | 142 | 143 | 144 | Column Metadata 145 | 146 | 147 | 148 | 149 | 150 | Subcolumns of the column 151 | 152 | 153 | 154 | 155 | 156 | 157 | One ColData can contain one column 158 | 159 | 160 | 161 | 162 | Describes the column attributes 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Reference url 171 | 172 | 173 | 174 | 175 | 176 | One Row can contain any number of columns 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | Row type section, summary, data row etc.. 197 | 198 | 199 | 200 | 201 | Report Group Income, Expense, COGS etc.. 202 | 203 | 204 | 205 | 206 | 207 | Group Header 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | Group Summary 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | List of rows 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | Specifies the Header of a Report, Time report was generated, parameters corresponding to the request 232 | 233 | 234 | 235 | 236 | Specifies the time at which report was generated 237 | 238 | 239 | 240 | 241 | Specifies the report name 242 | 243 | 244 | 245 | 246 | Specifies the report name 247 | 248 | 249 | 250 | 251 | Specifies the report is cash basis or accrual basis 252 | 253 | 254 | 255 | 256 | Start Period for which the report was generated 257 | 258 | 259 | 260 | 261 | End Period for which the report was generated 262 | 263 | 264 | 265 | 266 | Summarize columns by enumeration 267 | 268 | 269 | 270 | 271 | Specifies the currency code associated with the report, note that this is one place where this is just the currency code, not a reference to a currency object 272 | 273 | 274 | 275 | 276 | Specifies the customer id (comma separeted) for which the report is run this is just the id, not a reference to a customer object 277 | 278 | 279 | 280 | 281 | Specifies the vendor id (comma separeted) for which the report is run this is just the id, not a reference to a vendor object 282 | 283 | 284 | 285 | 286 | Specifies the employee id (comma separeted) for which the report is run this is just the id, not a reference to a employee object 287 | 288 | 289 | 290 | 291 | Specifies the product/service id (comma separeted) for which the report is run this is just the id, not a reference to a product/service object 292 | 293 | 294 | 295 | 296 | Specifies the class id (comma separeted) for which the report is run this is just the id, not a reference to a class object 297 | 298 | 299 | 300 | 301 | Specifies the Department id (comma separeted) for which the report is run this is just the id, not a reference to a Department object 302 | 303 | 304 | 305 | 306 | 307 | Describes the options used for the report 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | Report Response Type 320 | 321 | 322 | 323 | 324 | Report Header, contains the report options that were used to generate the report 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/xsd/3.56/SalesTax.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | Product: QBO 10 | Description: TaxRate details 11 | 12 | 13 | 14 | 15 | 16 | 17 | Product: QBO 18 | Description: TaxRate details 19 | 20 | 21 | 22 | 23 | 24 | 25 | Product: QBO 26 | Description: TaxRate details 27 | 28 | 29 | 30 | 31 | 32 | 33 | Product: QBO 34 | Description: TaxRate value 35 | 36 | 37 | 38 | 39 | 40 | 41 | Product: QBO 42 | Description: TaxAgency details 43 | 44 | 45 | 46 | 47 | 48 | 49 | Product: QBO 50 | Description: Default is SalesTax 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Describes SalesTax details 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Product: QBO 68 | Description: Describes the taxcode 69 | 70 | 71 | 72 | 73 | 74 | 75 | Product: QBO 76 | Description: Describes the taxcode Id, this is output only 77 | 78 | 79 | 80 | 81 | 82 | 83 | Product: QBO 84 | Description: TaxRate details 85 | 86 | 87 | 88 | 89 | 90 | Fault or Object should be returned 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Product: QBO 102 | Description: Enumeration of transaction type a given tax rate can be applied to 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/xsd/3.65/Report.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | Date macros enumeration 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Specifies the column type definition 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | List of all row types 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 95 | Describes the type 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Describes the Name 105 | 106 | 107 | 108 | 109 | Describes the Value 110 | 111 | 112 | 113 | 114 | 115 | 116 | List of columns 117 | 118 | 119 | 121 | 122 | Column of the report 123 | 124 | 125 | 126 | 127 | 128 | 129 | Describes a column 130 | 131 | 132 | 133 | 134 | Describes the column title name 135 | 136 | 137 | 138 | 139 | Describes the column type enumeration 140 | 141 | 142 | 143 | 144 | Column Metadata 145 | 146 | 147 | 148 | 149 | 150 | Subcolumns of the column 151 | 152 | 153 | 154 | 155 | 156 | 157 | One ColData can contain one column 158 | 159 | 160 | 161 | 162 | Describes the column attributes 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Reference url 171 | 172 | 173 | 174 | 175 | 176 | One Row can contain any number of columns 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | Row type section, summary, data row etc.. 197 | 198 | 199 | 200 | 201 | Report Group Income, Expense, COGS etc.. 202 | 203 | 204 | 205 | 206 | 207 | Group Header 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | Group Summary 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | List of rows 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | Specifies the Header of a Report, Time report was generated, parameters corresponding to the request 232 | 233 | 234 | 235 | 236 | Specifies the time at which report was generated 237 | 238 | 239 | 240 | 241 | Specifies the report name 242 | 243 | 244 | 245 | 246 | Specifies the report name 247 | 248 | 249 | 250 | 251 | Specifies the report is cash basis or accrual basis 252 | 253 | 254 | 255 | 256 | Start Period for which the report was generated 257 | 258 | 259 | 260 | 261 | End Period for which the report was generated 262 | 263 | 264 | 265 | 266 | Summarize columns by enumeration 267 | 268 | 269 | 270 | 271 | Specifies the currency code associated with the report, note that this is one place where this is just the currency code, not a reference to a currency object 272 | 273 | 274 | 275 | 276 | Specifies the customer id (comma separeted) for which the report is run this is just the id, not a reference to a customer object 277 | 278 | 279 | 280 | 281 | Specifies the vendor id (comma separeted) for which the report is run this is just the id, not a reference to a vendor object 282 | 283 | 284 | 285 | 286 | Specifies the employee id (comma separeted) for which the report is run this is just the id, not a reference to a employee object 287 | 288 | 289 | 290 | 291 | Specifies the product/service id (comma separeted) for which the report is run this is just the id, not a reference to a product/service object 292 | 293 | 294 | 295 | 296 | Specifies the class id (comma separeted) for which the report is run this is just the id, not a reference to a class object 297 | 298 | 299 | 300 | 301 | Specifies the Department id (comma separeted) for which the report is run this is just the id, not a reference to a Department object 302 | 303 | 304 | 305 | 306 | 307 | Describes the options used for the report 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | Report Response Type 320 | 321 | 322 | 323 | 324 | Report Header, contains the report options that were used to generate the report 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/xsd/3.65/SalesTax.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | Product: QBO 10 | Description: TaxRate details 11 | 12 | 13 | 14 | 15 | 16 | 17 | Product: QBO 18 | Description: TaxRate details 19 | 20 | 21 | 22 | 23 | 24 | 25 | Product: QBO 26 | Description: TaxRate details 27 | 28 | 29 | 30 | 31 | 32 | 33 | Product: QBO 34 | Description: TaxRate value 35 | 36 | 37 | 38 | 39 | 40 | 41 | Product: QBO 42 | Description: TaxAgency details 43 | 44 | 45 | 46 | 47 | 48 | 49 | Product: QBO 50 | Description: Default is SalesTax 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Describes SalesTax details 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Product: QBO 68 | Description: Describes the taxcode 69 | 70 | 71 | 72 | 73 | 74 | 75 | Product: QBO 76 | Description: Describes the taxcode Id, this is output only 77 | 78 | 79 | 80 | 81 | 82 | 83 | Product: QBO 84 | Description: TaxRate details 85 | 86 | 87 | 88 | 89 | 90 | Fault or Object should be returned 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Product: QBO 102 | Description: Enumeration of transaction type a given tax rate can be applied to 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/xsd/3.73/Report.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | Date macros enumeration 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Specifies the column type definition 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | List of all row types 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 95 | Describes the type 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Describes the Name 105 | 106 | 107 | 108 | 109 | Describes the Value 110 | 111 | 112 | 113 | 114 | 115 | 116 | List of columns 117 | 118 | 119 | 121 | 122 | Column of the report 123 | 124 | 125 | 126 | 127 | 128 | 129 | Describes a column 130 | 131 | 132 | 133 | 134 | Describes the column title name 135 | 136 | 137 | 138 | 139 | Describes the column type enumeration 140 | 141 | 142 | 143 | 144 | Column Metadata 145 | 146 | 147 | 148 | 149 | 150 | Subcolumns of the column 151 | 152 | 153 | 154 | 155 | 156 | 157 | One ColData can contain one column 158 | 159 | 160 | 161 | 162 | Describes the column attributes 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Reference url 171 | 172 | 173 | 174 | 175 | 176 | One Row can contain any number of columns 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | Row type section, summary, data row etc.. 197 | 198 | 199 | 200 | 201 | Report Group Income, Expense, COGS etc.. 202 | 203 | 204 | 205 | 206 | 207 | Group Header 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | Group Summary 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | List of rows 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | Specifies the Header of a Report, Time report was generated, parameters corresponding to the request 232 | 233 | 234 | 235 | 236 | Specifies the time at which report was generated 237 | 238 | 239 | 240 | 241 | Specifies the report name 242 | 243 | 244 | 245 | 246 | Specifies the report name 247 | 248 | 249 | 250 | 251 | Specifies the report is cash basis or accrual basis 252 | 253 | 254 | 255 | 256 | Start Period for which the report was generated 257 | 258 | 259 | 260 | 261 | End Period for which the report was generated 262 | 263 | 264 | 265 | 266 | Summarize columns by enumeration 267 | 268 | 269 | 270 | 271 | Specifies the currency code associated with the report, note that this is one place where this is just the currency code, not a reference to a currency object 272 | 273 | 274 | 275 | 276 | Specifies the customer id (comma separeted) for which the report is run this is just the id, not a reference to a customer object 277 | 278 | 279 | 280 | 281 | Specifies the vendor id (comma separeted) for which the report is run this is just the id, not a reference to a vendor object 282 | 283 | 284 | 285 | 286 | Specifies the employee id (comma separeted) for which the report is run this is just the id, not a reference to a employee object 287 | 288 | 289 | 290 | 291 | Specifies the product/service id (comma separeted) for which the report is run this is just the id, not a reference to a product/service object 292 | 293 | 294 | 295 | 296 | Specifies the class id (comma separeted) for which the report is run this is just the id, not a reference to a class object 297 | 298 | 299 | 300 | 301 | Specifies the Department id (comma separeted) for which the report is run this is just the id, not a reference to a Department object 302 | 303 | 304 | 305 | 306 | 307 | Describes the options used for the report 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | Report Response Type 320 | 321 | 322 | 323 | 324 | Report Header, contains the report options that were used to generate the report 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/xsd/3.73/SalesTax.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | Product: QBO 10 | Description: TaxRate details 11 | 12 | 13 | 14 | 15 | 16 | 17 | Product: QBO 18 | Description: TaxRate details 19 | 20 | 21 | 22 | 23 | 24 | 25 | Product: QBO 26 | Description: TaxRate details 27 | 28 | 29 | 30 | 31 | 32 | 33 | Product: QBO 34 | Description: TaxRate value 35 | 36 | 37 | 38 | 39 | 40 | 41 | Product: QBO 42 | Description: TaxAgency details 43 | 44 | 45 | 46 | 47 | 48 | 49 | Product: QBO 50 | Description: Default is SalesTax 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Describes SalesTax details 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Product: QBO 68 | Description: Describes the taxcode 69 | 70 | 71 | 72 | 73 | 74 | 75 | Product: QBO 76 | Description: Describes the taxcode Id, this is output only 77 | 78 | 79 | 80 | 81 | 82 | 83 | Product: QBO 84 | Description: TaxRate details 85 | 86 | 87 | 88 | 89 | 90 | Fault or Object should be returned 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Product: QBO 102 | Description: Enumeration of transaction type a given tax rate can be applied to 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/xsd/3.75/Report.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | Date macros enumeration 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Specifies the column type definition 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | List of all row types 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 95 | Describes the type 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Describes the Name 105 | 106 | 107 | 108 | 109 | Describes the Value 110 | 111 | 112 | 113 | 114 | 115 | 116 | List of columns 117 | 118 | 119 | 121 | 122 | Column of the report 123 | 124 | 125 | 126 | 127 | 128 | 129 | Describes a column 130 | 131 | 132 | 133 | 134 | Describes the column title name 135 | 136 | 137 | 138 | 139 | Describes the column type enumeration 140 | 141 | 142 | 143 | 144 | Column Metadata 145 | 146 | 147 | 148 | 149 | 150 | Subcolumns of the column 151 | 152 | 153 | 154 | 155 | 156 | 157 | One ColData can contain one column 158 | 159 | 160 | 161 | 162 | Describes the column attributes 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Reference url 171 | 172 | 173 | 174 | 175 | 176 | One Row can contain any number of columns 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | Row type section, summary, data row etc.. 197 | 198 | 199 | 200 | 201 | Report Group Income, Expense, COGS etc.. 202 | 203 | 204 | 205 | 206 | 207 | Group Header 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | Group Summary 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | List of rows 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | Specifies the Header of a Report, Time report was generated, parameters corresponding to the request 232 | 233 | 234 | 235 | 236 | Specifies the time at which report was generated 237 | 238 | 239 | 240 | 241 | Specifies the report name 242 | 243 | 244 | 245 | 246 | Specifies the report name 247 | 248 | 249 | 250 | 251 | Specifies the report is cash basis or accrual basis 252 | 253 | 254 | 255 | 256 | Start Period for which the report was generated 257 | 258 | 259 | 260 | 261 | End Period for which the report was generated 262 | 263 | 264 | 265 | 266 | Summarize columns by enumeration 267 | 268 | 269 | 270 | 271 | Specifies the currency code associated with the report, note that this is one place where this is just the currency code, not a reference to a currency object 272 | 273 | 274 | 275 | 276 | Specifies the customer id (comma separeted) for which the report is run this is just the id, not a reference to a customer object 277 | 278 | 279 | 280 | 281 | Specifies the vendor id (comma separeted) for which the report is run this is just the id, not a reference to a vendor object 282 | 283 | 284 | 285 | 286 | Specifies the employee id (comma separeted) for which the report is run this is just the id, not a reference to a employee object 287 | 288 | 289 | 290 | 291 | Specifies the product/service id (comma separeted) for which the report is run this is just the id, not a reference to a product/service object 292 | 293 | 294 | 295 | 296 | Specifies the class id (comma separeted) for which the report is run this is just the id, not a reference to a class object 297 | 298 | 299 | 300 | 301 | Specifies the Department id (comma separeted) for which the report is run this is just the id, not a reference to a Department object 302 | 303 | 304 | 305 | 306 | 307 | Describes the options used for the report 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | Report Response Type 320 | 321 | 322 | 323 | 324 | Report Header, contains the report options that were used to generate the report 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | -------------------------------------------------------------------------------- /QuickBooksSharp.CodeGen/xsd/3.75/SalesTax.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | Product: QBO 10 | Description: TaxRate details 11 | 12 | 13 | 14 | 15 | 16 | 17 | Product: QBO 18 | Description: TaxRate details 19 | 20 | 21 | 22 | 23 | 24 | 25 | Product: QBO 26 | Description: TaxRate details 27 | 28 | 29 | 30 | 31 | 32 | 33 | Product: QBO 34 | Description: TaxRate value 35 | 36 | 37 | 38 | 39 | 40 | 41 | Product: QBO 42 | Description: TaxAgency details 43 | 44 | 45 | 46 | 47 | 48 | 49 | Product: QBO 50 | Description: Default is SalesTax 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Describes SalesTax details 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Product: QBO 68 | Description: Describes the taxcode 69 | 70 | 71 | 72 | 73 | 74 | 75 | Product: QBO 76 | Description: Describes the taxcode Id, this is output only 77 | 78 | 79 | 80 | 81 | 82 | 83 | Product: QBO 84 | Description: TaxRate details 85 | 86 | 87 | 88 | 89 | 90 | Fault or Object should be returned 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Product: QBO 102 | Description: Enumeration of transaction type a given tax rate can be applied to 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /QuickBooksSharp.Tests/AuthenticationService_Tests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace QuickBooksSharp.Tests 6 | { 7 | [TestClass] 8 | public class AuthenticationService_Tests 9 | { 10 | private AuthenticationService _service; 11 | 12 | [TestInitialize] 13 | public void Initialize() 14 | { 15 | _service = new AuthenticationService(); 16 | } 17 | 18 | [TestMethod] 19 | public void GenerateAuthorizationPromptUrl_Works() 20 | { 21 | string url = _service.GenerateAuthorizationPromptUrl(TestHelper.ClientId, new[] { "com.intuit.quickbooks.accounting" }, TestHelper.RedirectUri, Guid.NewGuid().ToString()); 22 | Assert.IsNotNull(url); 23 | } 24 | 25 | [TestMethod] 26 | [Ignore("Requires manual input of code")] 27 | public async Task GetOAuthTokenAsync_Works() 28 | { 29 | var token = await _service.GetOAuthTokenAsync(TestHelper.ClientId, TestHelper.ClientSecret, "", TestHelper.RedirectUri); 30 | Assert.IsNotNull(token.access_token); 31 | Assert.IsNotNull(token.refresh_token); 32 | Assert.IsTrue(token.expires_in > TimeSpan.Zero); 33 | Assert.IsTrue(token.x_refresh_token_expires_in > TimeSpan.Zero); 34 | } 35 | 36 | 37 | [TestMethod] 38 | public async Task RefreshOAuthTokenAsync_Works() 39 | { 40 | var token = await _service.RefreshOAuthTokenAsync(TestHelper.ClientId, TestHelper.ClientSecret, TestHelper.RefreshToken); 41 | Assert.IsNotNull(token.access_token); 42 | Assert.IsTrue(token.expires_in > TimeSpan.Zero); 43 | Assert.IsNotNull(token.refresh_token); 44 | } 45 | 46 | [TestMethod] 47 | [Ignore("Requires manual input of token")] 48 | public async Task RevokeOAuthTokenAsync_Works() 49 | { 50 | await _service.RevokeOAuthTokenAsync(TestHelper.ClientId, TestHelper.ClientSecret, ""); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /QuickBooksSharp.Tests/DataServiceTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using QuickBooksSharp.Entities; 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace QuickBooksSharp.Tests 10 | { 11 | [TestClass] 12 | public class DataServiceTests : ServiceTestBase 13 | { 14 | private DataService _service; 15 | private readonly Type[] _entityTypes = new[] 16 | { 17 | typeof(Account), 18 | typeof(BillPayment), 19 | typeof(Bill), 20 | typeof(Budget), 21 | typeof(Class), 22 | typeof(CompanyCurrency), 23 | typeof(CompanyInfo), 24 | typeof(CreditCardPayment), 25 | typeof(CreditMemo), 26 | typeof(Customer), 27 | typeof(CustomerType), 28 | typeof(Deposit), 29 | typeof(Employee), 30 | typeof(Estimate), 31 | typeof(ExchangeRate), 32 | typeof(Invoice), 33 | typeof(Item), 34 | typeof(JournalEntry), 35 | typeof(Department), 36 | typeof(PaymentMethod), 37 | typeof(Payment), 38 | typeof(Preferences), 39 | typeof(PurchaseOrder), 40 | typeof(Purchase), 41 | typeof(RefundReceipt), 42 | typeof(ReimburseCharge), 43 | typeof(SalesReceipt), 44 | typeof(TaxAgency), 45 | typeof(TaxClassification), 46 | typeof(TaxCode), 47 | typeof(TaxRate), 48 | typeof(Term), 49 | typeof(TimeActivity), 50 | typeof(Transfer), 51 | typeof(VendorCredit), 52 | typeof(Vendor), 53 | }; 54 | 55 | [TestInitialize] 56 | public async Task Initialize() 57 | { 58 | var accessToken = await GetAccessTokenAsync(); 59 | _service = new DataService(accessToken, TestHelper.RealmId, true); 60 | } 61 | 62 | [TestMethod] 63 | public async Task QueryCustomers() 64 | { 65 | var res = await _service.QueryAsync("SELECT * FROM Customer"); 66 | Assert.IsNotNull(res); 67 | Assert.IsNull(res.Fault); 68 | Assert.IsNotNull(res.Time); 69 | Assert.IsNotNull(res.Response); 70 | Assert.IsNull(res.Response.Fault); 71 | Assert.IsNotNull(res.Response.StartPosition); 72 | Assert.IsNotNull(res.Response.Entities); 73 | Assert.IsNotNull(res.Response.MaxResults); 74 | Assert.IsNotNull(res.Response.Entities[0].Id); 75 | Assert.IsNotNull(res.Response.Entities[0].DisplayName); 76 | } 77 | 78 | [TestMethod] 79 | public async Task QueryCustomerCount() 80 | { 81 | var res = await _service.QueryCountAsync("SELECT COUNT(*) FROM Customer"); 82 | Assert.IsNotNull(res); 83 | Assert.IsNull(res.Fault); 84 | Assert.IsNotNull(res.Time); 85 | Assert.IsNotNull(res.Response); 86 | Assert.IsNull(res.Response.Fault); 87 | Assert.IsNotNull(res.Response.TotalCount); 88 | } 89 | 90 | [TestMethod] 91 | public async Task CreateUpdateParseUpdateCustomer() 92 | { 93 | string uniquifier = DateTime.Now.Ticks.ToString(); 94 | var resCreate = await _service.PostAsync(new Customer 95 | { 96 | DisplayName = $"Test - My display name {uniquifier}", 97 | Suffix = "Jr", 98 | Title = "Mr", 99 | MiddleName = "Test - My middle name", 100 | FamilyName = "Test - My family name", 101 | GivenName = "Test - My given name", 102 | }); 103 | Assert.IsNotNull(resCreate); 104 | Assert.IsNull(resCreate.Fault); 105 | Assert.IsNotNull(resCreate.Time); 106 | Assert.IsNotNull(resCreate.Response); 107 | Assert.IsNotNull(resCreate.Response.Id); 108 | Assert.IsNotNull(resCreate.Response.DisplayName); 109 | 110 | var resSparseUpdate = await _service.PostAsync(new Customer 111 | { 112 | Id = resCreate.Response.Id, 113 | SyncToken = resCreate.Response.SyncToken, 114 | GivenName = $"{resCreate.Response.GivenName} - sparsed", 115 | sparse = true 116 | }); 117 | Assert.IsNotNull(resSparseUpdate); 118 | Assert.IsNull(resSparseUpdate.Fault); 119 | Assert.IsNotNull(resSparseUpdate.Time); 120 | Assert.IsNotNull(resSparseUpdate.Response); 121 | Assert.IsNotNull(resSparseUpdate.Response.Id); 122 | Assert.AreEqual(resSparseUpdate.Response.DisplayName, resCreate.Response.DisplayName); 123 | Assert.AreNotEqual(resSparseUpdate.Response.GivenName, resCreate.Response.GivenName); 124 | 125 | var c = resSparseUpdate.Response; 126 | c.FamilyName = $"{resSparseUpdate.Response.FamilyName} - full"; 127 | c.sparse = false; 128 | var resFullUpdate = await _service.PostAsync(resSparseUpdate.Response); 129 | Assert.IsNotNull(resFullUpdate); 130 | Assert.IsNull(resFullUpdate.Fault); 131 | Assert.IsNotNull(resFullUpdate.Time); 132 | Assert.IsNotNull(resFullUpdate.Response); 133 | Assert.IsNotNull(resFullUpdate.Response.Id); 134 | Assert.AreEqual(resFullUpdate.Response.DisplayName, resCreate.Response.DisplayName); 135 | Assert.AreNotEqual(resFullUpdate.Response.FamilyName, resCreate.Response.FamilyName); 136 | } 137 | 138 | [TestMethod] 139 | public async Task CreateDeleteInvoice() 140 | { 141 | var queryCustomerRes = await _service.QueryAsync("SELECT * FROM Customer MAXRESULTS 1"); 142 | var queryAccountRes = await _service.QueryAsync("SELECT * FROM Account MAXRESULTS 1"); 143 | 144 | var resCreate = await _service.PostAsync(new Invoice 145 | { 146 | CustomerRef = new ReferenceType 147 | { 148 | value = queryCustomerRes.Response.Entities[0].Id 149 | }, 150 | Line = new[] 151 | { 152 | new Line 153 | { 154 | Amount = 100.0m, 155 | DetailType = LineDetailTypeEnum.SalesItemLineDetail, 156 | SalesItemLineDetail = new SalesItemLineDetail 157 | { 158 | ItemAccountRef = new ReferenceType 159 | { 160 | value = queryAccountRes.Response.Entities[0].Id 161 | } 162 | } 163 | } 164 | } 165 | }); 166 | Assert.IsNotNull(resCreate); 167 | Assert.IsNull(resCreate.Fault); 168 | Assert.IsNotNull(resCreate.Time); 169 | Assert.IsNotNull(resCreate.Response); 170 | Assert.IsNotNull(resCreate.Response.Id); 171 | 172 | var resDelete = await _service.PostAsync(new Invoice { Id = resCreate.Response.Id, SyncToken = resCreate.Response.SyncToken }, OperationEnum.delete); 173 | } 174 | 175 | [TestMethod] 176 | public async Task QueryEntitiesCount() 177 | { 178 | await Task.WhenAll(_entityTypes 179 | .Where(t2 => !new[] 180 | { 181 | typeof(TaxPayment),//Only available on AU/UK companies 182 | typeof(ExchangeRate),//Message=Error processing query https://help.developer.intuit.com/s/question/0D54R00007pirJESAY/the-following-query-results-in-an-error-select-count-from-exchangerateerror-returned-from-api-error-processing-query 183 | typeof(CustomerType),//Detail=Dear entity developer, pl implement count query https://help.developer.intuit.com/s/question/0D54R00007pirJFSAY/select-count-from-customertype-returns-an-error 184 | typeof(CompanyInfo),//Total count not returned 185 | }.Contains(t2)) 186 | .Select(async t => 187 | { 188 | try 189 | { 190 | string entityName = t == typeof(QbTask) ? "Task" : t.Name; 191 | var res = await _service.QueryCountAsync($"SELECT COUNT(*) FROM {entityName}"); 192 | Assert.IsNotNull(res); 193 | Assert.IsNull(res.Fault); 194 | Assert.IsNotNull(res.Time); 195 | Assert.IsNotNull(res.Response); 196 | Assert.IsNull(res.Response.Fault); 197 | Assert.IsNotNull(res.Response.TotalCount); 198 | } 199 | catch (QuickBooksException ex) when (ex.ResponseContent.Contains("Metadata not found for Entity")) 200 | { 201 | //Ignore entities that don't support querying 202 | } 203 | })); 204 | } 205 | 206 | [TestMethod] 207 | public async Task QueryEntities() 208 | { 209 | var entities = new ConcurrentQueue(); 210 | await Task.WhenAll(_entityTypes 211 | .Where(t2 => !new[] 212 | { 213 | typeof(Employee),//API error complaing about inactive employee 214 | }.Contains(t2)) 215 | .Select(async t => 216 | { 217 | try 218 | { 219 | string entityName = t == typeof(QbTask) ? "Task" : t.Name; 220 | var res = await _service.QueryAsync($"SELECT * FROM {entityName}"); 221 | Assert.IsNotNull(res); 222 | Assert.IsNull(res.Fault); 223 | Assert.IsNotNull(res.Time); 224 | Assert.IsNull(res.Response.Fault); 225 | Assert.IsNotNull(res.Response); 226 | //it seems that if there are 0 rows, the following are null 227 | if (res.Response.StartPosition != null) 228 | { 229 | Assert.IsNotNull(res.Response.Entities); 230 | Assert.IsNotNull(res.Response.MaxResults); 231 | if (res.Response.Entities.FirstOrDefault()?.Id != null) 232 | { 233 | //Built-in tax code entities can have non numeric id TAX or NON 234 | //See https://help.developer.intuit.com/s/question/0D74R000004jvUi 235 | entities.Enqueue(res.Response.Entities.FirstOrDefault(i => long.TryParse(i.Id, out _))); 236 | } 237 | } 238 | } 239 | catch (QuickBooksException ex) when (ex.ResponseContent.Contains("Metadata not found for Entity")) 240 | { 241 | //Ignore entities that don't support querying 242 | } 243 | })); 244 | 245 | await Task.WhenAll(entities 246 | //https://help.developer.intuit.com/s/question/0D54R00007pisJuSAI/taxcode-id-tax-instead-of-numeric-ids 247 | .Select(async e => 248 | { 249 | var resOne = await _service.GetAsync(e.Id, e.GetType()); 250 | Assert.IsNotNull(resOne); 251 | Assert.IsNotNull(resOne.Response); 252 | Assert.IsNotNull(resOne.Response.Id); 253 | })); 254 | } 255 | 256 | [TestMethod] 257 | public async Task GetProfitAndLossReport() 258 | { 259 | var r = await _service.GetReportAsync("ProfitAndLoss", new() 260 | { 261 | { "accounting_method", "Accrual" }, 262 | { "date_macro", "Last Fiscal Year" } 263 | }); 264 | Assert.IsNotNull(r); 265 | Assert.IsNotNull(r.Header?.ReportName); 266 | Assert.IsTrue(r.Rows.Row.Length != 0); 267 | Assert.IsTrue(r.Columns.Column.Length != 0); 268 | } 269 | 270 | [TestMethod] 271 | public async Task GetJournalReport() 272 | { 273 | var r = await _service.GetReportAsync("JournalReport", new() 274 | { 275 | { "date_macro", "Last Fiscal Year" } 276 | }); 277 | Assert.IsNotNull(r); 278 | Assert.IsNotNull(r.Header?.ReportName); 279 | Assert.IsTrue(r.Rows.Row.Length != 0); 280 | Assert.IsTrue(r.Columns.Column.Length != 0); 281 | } 282 | 283 | [TestMethod] 284 | public async Task GetTransactionDetailByAccount() 285 | { 286 | var includedColumns = new[] 287 | { 288 | "account_name", 289 | "create_by", 290 | "create_date", 291 | "credit_amt", 292 | "credit_home_amt", 293 | "currency", 294 | "cust_name", 295 | "debt_amt", 296 | "debt_home_amt", 297 | "dept_name", 298 | "doc_num", 299 | "due_date", 300 | "emp_name", 301 | "exch_rate", 302 | "foreign_net_amount", 303 | "foreign_tax_amount", 304 | "home_net_amount", 305 | "home_tax_amount", 306 | "net_amount", 307 | "tax_amount", 308 | "is_adj", 309 | "is_ap_paid", 310 | "is_ar_paid", 311 | "is_cleared", 312 | "item_name", 313 | "klass_name", 314 | "last_mod_by", 315 | "last_mod_date", 316 | "memo", 317 | "nat_foreign_amount", 318 | "nat_foreign_open_bal", 319 | "nat_home_open_bal", 320 | "nat_open_bal", 321 | "olb_status", 322 | "pmt_mthd", 323 | "quantity", 324 | "rate", 325 | "split_acc", 326 | "subt_nat_home_amount", 327 | "subt_nat_amount", 328 | "tax_code_name", 329 | "tax_type", 330 | "tx_date", 331 | "txn_type", 332 | "vend_name", 333 | }; 334 | var r = await _service.GetReportAsync("TransactionDetailByAccount", new() 335 | { 336 | { "columns", string.Join(",", includedColumns) }, 337 | { "transaction_type", "post" }, 338 | { "groupby", "none" }, 339 | { "accounting_method", "Cash" }, 340 | { "sort_by", "create_date" }, 341 | { "sort_order", "descend" }, 342 | { "start_date", DateTime.Today.AddMonths(-1).ToString("yyyy-MM-dd") }, 343 | { "end_date", DateTime.Today.ToString("yyyy-MM-dd") }, 344 | }); 345 | Assert.IsNotNull(r); 346 | Assert.IsNotNull(r.Header?.ReportName); 347 | Assert.IsTrue(r.Columns.Column.Length != 0); 348 | } 349 | 350 | 351 | [TestMethod] 352 | public async Task GetCDC() 353 | { 354 | var entityTypes = _entityTypes.Except(new[] 355 | { 356 | typeof(TaxPayment), //UK/AU only 357 | 358 | //not all entities are supported by CDC 359 | typeof(QbdtEntityIdMapping), 360 | typeof(ConvenienceFeeDetail), 361 | typeof(EmailDeliveryInfo), 362 | typeof(Tag), 363 | typeof(FixedAsset), 364 | typeof(MasterAccount), 365 | typeof(StatementCharge), 366 | typeof(JournalCode), 367 | typeof(SalesOrder), 368 | typeof(SalesRep), 369 | typeof(PriceLevel), 370 | typeof(PriceLevelPerItem), 371 | typeof(CustomerMsg), 372 | typeof(InventorySite), 373 | typeof(ShipMethod), 374 | typeof(QbTask), 375 | typeof(UOM), 376 | typeof(TemplateName), 377 | typeof(TDSMetadata), 378 | typeof(BooleanTypeCustomFieldDefinition), 379 | typeof(DateTypeCustomFieldDefinition), 380 | typeof(NumberTypeCustomFieldDefinition), 381 | typeof(StringTypeCustomFieldDefinition), 382 | typeof(ChargeCredit), 383 | typeof(JobType), 384 | typeof(OtherName), 385 | typeof(Status), 386 | typeof(SyncActivity), 387 | typeof(TaxAgency), 388 | typeof(TaxClassification), 389 | typeof(TaxService), 390 | typeof(User), 391 | typeof(VendorType), 392 | typeof(Currency), 393 | }) 394 | .OrderBy(t => t.Name); 395 | 396 | await Task.WhenAll(entityTypes.Select(async t => 397 | { 398 | var res = await _service.GetCDCAsync(DateTimeOffset.UtcNow.AddDays(-10), new[] { t.Name }); 399 | Assert.IsNotNull(res); 400 | Assert.IsNotNull(res.Response); 401 | Assert.IsTrue(res.Response.QueryResponse.Length == 1); 402 | var queryResponse = res.Response.QueryResponse.First(); 403 | if (queryResponse.IntuitObjects != null) 404 | Assert.IsTrue(queryResponse.IntuitObjects.All(o => o.GetType() == t)); 405 | })); 406 | 407 | var resAll = await _service.GetCDCAsync(DateTimeOffset.UtcNow.AddDays(-10), entityTypes.Select(t => t.Name)); 408 | Assert.IsNotNull(resAll); 409 | Assert.IsNotNull(resAll.Response); 410 | Assert.IsTrue(resAll.Response.QueryResponse.Length == entityTypes.Count()); 411 | } 412 | 413 | [TestMethod] 414 | public async Task BatchQuery() 415 | { 416 | var response = await _service.BatchAsync(new IntuitBatchRequest 417 | { 418 | BatchItemRequest = new[] 419 | { 420 | new BatchItemRequest 421 | { 422 | bId = Guid.NewGuid().ToString(), 423 | Query = "SELECT * FROM Bill MAXRESULTS 30", 424 | }, 425 | new BatchItemRequest 426 | { 427 | bId = Guid.NewGuid().ToString(), 428 | Query = "SELECT * FROM Invoice MAXRESULTS 30", 429 | } 430 | } 431 | }); 432 | Assert.IsTrue(response.Response.ElementAt(0).QueryResponse.Bill.Length > 0); 433 | Assert.IsTrue(response.Response.ElementAt(1).QueryResponse.Invoice.Length > 0); 434 | } 435 | 436 | [TestMethod] 437 | public async Task GetInvoicePDFAsync() 438 | { 439 | var response = await _service.QueryAsync("SELECT * FROM Invoice MAXRESULTS 1"); 440 | 441 | Assert.IsTrue(response.Response.Entities.Length > 0); 442 | 443 | var invoidePdfStream = await _service.GetInvoicePDFAsync(response.Response.Entities[0].Id); 444 | 445 | Assert.IsNotNull(invoidePdfStream); 446 | Assert.IsNotNull(invoidePdfStream.Length > 0); 447 | } 448 | 449 | [TestMethod] 450 | public async Task CreatePaymentAndVoidAsync() 451 | { 452 | // GET an Open Invoice 453 | var invoiceResponse = await _service.QueryAsync( 454 | "SELECT * FROM Invoice " + 455 | "WHERE Balance > '0' " + 456 | "ORDERBY DueDate DESC, Balance DESC " + 457 | "MAXRESULTS 1"); 458 | 459 | if (invoiceResponse.Response.Entities.Length == 0) 460 | { 461 | Assert.Fail("No Invoices returned"); 462 | } 463 | 464 | var invoice = invoiceResponse.Response.Entities[0]; 465 | 466 | // Create a Payment for that Invoice 467 | var payment = new Payment() 468 | { 469 | CustomerRef = invoice.CustomerRef, 470 | TotalAmt = 1, 471 | Line = new[] { 472 | new Line 473 | { 474 | Amount = 1, 475 | LinkedTxn = new LinkedTxn[] { new LinkedTxn 476 | { 477 | TxnId = invoice.Id, 478 | TxnType = $"{QboEntityTypeEnum.INVOICE}" 479 | } } 480 | } 481 | }, 482 | PrivateNote = "Payment made by QuickBooksSharp" 483 | }; 484 | 485 | var paymentResponse = await _service.PostAsync(payment); 486 | 487 | Assert.IsTrue(paymentResponse.Response != null); 488 | 489 | // Void Payment 490 | var voidPayment = new Payment() 491 | { 492 | Id = paymentResponse.Response.Id, 493 | SyncToken = paymentResponse.Response.SyncToken, 494 | sparse = true, 495 | PrivateNote = "Payment voided by QuickBooksSharp" 496 | }; 497 | 498 | var voidPaymentResponse = await _service.PostAsync(voidPayment, OperationEnum.update, OperationEnum.@void); 499 | 500 | Assert.IsNotNull(voidPaymentResponse.Response); 501 | } 502 | 503 | [TestMethod] 504 | public async Task CreateTaxCodeAndRateAsync() 505 | { 506 | var taxAgencyName = "Test Tax Agency"; 507 | var taxAgency = await GetTaxAgencyAsync(taxAgencyName); 508 | taxAgency ??= await CreateTaxAgency(taxAgencyName); 509 | 510 | var guid = Guid.NewGuid(); 511 | var taxCodeName = $"Test Tax Code {guid}"; 512 | var taxRateName = $"Test Tax Rate {guid}"; 513 | var taxRate = 10; 514 | 515 | var taxServiceResponse = await _service.PostTaxServiceAsync(new TaxService 516 | { 517 | TaxCode = taxCodeName, 518 | TaxRateDetails = new List 519 | { 520 | new() 521 | { 522 | RateValue = taxRate, 523 | TaxAgencyId = taxAgency.Id, 524 | TaxRateName = taxRateName 525 | } 526 | }.ToArray() 527 | }); 528 | 529 | Assert.IsNotNull(taxServiceResponse.TaxCodeId); 530 | Assert.AreEqual(taxServiceResponse.TaxCode, taxCodeName); 531 | 532 | Assert.IsNotNull(taxServiceResponse.TaxRateDetails); 533 | Assert.AreEqual(taxServiceResponse.TaxRateDetails[0].TaxRateName, taxRateName); 534 | Assert.IsNotNull(taxServiceResponse.TaxRateDetails[0].TaxRateId); 535 | Assert.AreEqual(taxServiceResponse.TaxRateDetails[0].RateValue, taxRate); 536 | } 537 | 538 | private async Task GetTaxAgencyAsync(string taxAgencyName) 539 | { 540 | var result = await _service.QueryAsync("SELECT * FROM TaxAgency"); 541 | var taxAgencies = result.Response.Entities.ToList(); 542 | return taxAgencies.Find(ta => ta.DisplayName == taxAgencyName); 543 | } 544 | 545 | private async Task CreateTaxAgency(string taxAgencyName) 546 | { 547 | var result = await _service.PostAsync(new TaxAgency 548 | { 549 | DisplayName = taxAgencyName, 550 | Active = true 551 | }); 552 | return result.Response; 553 | } 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /QuickBooksSharp.Tests/QuickBooksSharp.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /QuickBooksSharp.Tests/QuickBooksUrlTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace QuickBooksSharp.Tests 4 | { 5 | [TestClass] 6 | public class QuickBooksUrlTests 7 | { 8 | [TestMethod] 9 | public void ShouldCreateSandboxUrl() 10 | { 11 | var url = QuickBooksUrl.Build(true, 123); 12 | 13 | Assert.IsNotNull(url); 14 | Assert.AreEqual(url.ToUri().AbsoluteUri, $"https://sandbox-quickbooks.api.intuit.com/v3/company/123?minorversion={QuickBooksUrl.MinorVersion}"); 15 | 16 | } 17 | 18 | [TestMethod] 19 | public void ShouldCreateProductionUrl() 20 | { 21 | var url = QuickBooksUrl.Build(false, 123); 22 | 23 | Assert.IsNotNull(url); 24 | Assert.AreEqual(url.ToUri().AbsoluteUri, $"https://quickbooks.api.intuit.com/v3/company/123?minorversion={QuickBooksUrl.MinorVersion}"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /QuickBooksSharp.Tests/RunPolicyTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using QuickBooksSharp.Entities; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace QuickBooksSharp.Tests 7 | { 8 | [TestClass] 9 | [Ignore("Tests should be run manually")] 10 | public class RunPolicyTests : ServiceTestBase 11 | { 12 | private string _accessToken; 13 | 14 | [TestInitialize] 15 | public async Task Initialize() 16 | { 17 | _accessToken = await GetAccessTokenAsync(); 18 | } 19 | 20 | private async Task IssueManyRequests(IRunPolicy policy) 21 | { 22 | var svc = new DataService(_accessToken, TestHelper.RealmId, true, policy); 23 | await Parallel.ForEachAsync(Enumerable.Range(1, 1000), new ParallelOptions { MaxDegreeOfParallelism = 50 }, async (i, token) => 24 | { 25 | var res = await svc.QueryAsync("SELECT * FROM Customer WHERE Id = '1'"); 26 | Assert.IsNotNull(res); 27 | Assert.IsNull(res.Fault); 28 | Assert.IsNotNull(res.Time); 29 | Assert.IsNotNull(res.Response); 30 | }); 31 | } 32 | 33 | [TestMethod] 34 | public async Task NoRetryShouldFailWhenTooManyRequests() 35 | { 36 | QuickBooksException ex = null; 37 | 38 | try 39 | { 40 | await IssueManyRequests(new NoRetryRunPolicy()); 41 | } 42 | catch (QuickBooksException e) 43 | { 44 | ex = e; 45 | } 46 | 47 | Assert.IsNotNull(ex); 48 | Assert.IsTrue(ex.IsRateLimit); 49 | } 50 | 51 | [TestMethod] 52 | public async Task RetryShouldSucceedWhenTooManyRequests() 53 | { 54 | QuickBooksException ex = null; 55 | 56 | try 57 | { 58 | await IssueManyRequests(new SimpleRetryRunPolicy()); 59 | } 60 | catch (QuickBooksException e) 61 | { 62 | ex = e; 63 | } 64 | 65 | Assert.IsNull(ex); 66 | } 67 | 68 | [TestMethod] 69 | public async Task MaxConccurencyRetryShouldSucceedWhenTooManyRequests() 70 | { 71 | QuickBooksException ex = null; 72 | 73 | try 74 | { 75 | await IssueManyRequests(new MaxConcurrencyRetryRunPolicy()); 76 | } 77 | catch (QuickBooksException e) 78 | { 79 | ex = e; 80 | } 81 | 82 | Assert.IsNull(ex); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /QuickBooksSharp.Tests/ServiceTestBase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace QuickBooksSharp.Tests 4 | { 5 | public abstract class ServiceTestBase 6 | { 7 | protected async Task GetAccessTokenAsync() 8 | { 9 | var res = await new AuthenticationService().RefreshOAuthTokenAsync(TestHelper.ClientId, TestHelper.ClientSecret, TestHelper.RefreshToken); 10 | if (res.refresh_token != TestHelper.RefreshToken) 11 | TestHelper.PersistNewRefreshToken(res.refresh_token); 12 | return res.access_token; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /QuickBooksSharp.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace QuickBooksSharp.Tests 4 | { 5 | /// 6 | /// https://developer.intuit.com/app/developer/playground makes it easy to generate values from the variables below 7 | /// 8 | public class TestHelper 9 | { 10 | /// 11 | /// Used to run AuthenticationService tests 12 | /// 13 | public static string ClientId = GetEnvVar("QUICKBOOKS_SHARP_CLIENT_ID"); 14 | 15 | public static string ClientSecret = GetEnvVar("QUICKBOOKS_SHARP_CLIENT_SECRET"); 16 | 17 | public static string RedirectUri = GetEnvVar("QUICKBOOKS_SHARP_OAUTH_REDIRECT_URI"); 18 | 19 | public static long RealmId = long.Parse(GetEnvVar("QUICKBOOKS_SHARP_REALMID")); 20 | 21 | public static string RefreshToken = GetEnvVar("QUICKBOOKS_SHARP_REFRESH_TOKEN"); 22 | 23 | public static void PersistNewRefreshToken(string refreshToken) => Environment.SetEnvironmentVariable("QUICKBOOKS_SHARP_REFRESH_TOKEN", refreshToken, EnvironmentVariableTarget.User); 24 | 25 | private static string GetEnvVar(string name) => 26 | Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.User) 27 | ?? Environment.GetEnvironmentVariable(name) 28 | ?? throw new Exception($"Environment {name} is not defined"); 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /QuickBooksSharp.Tests/WebhookEventTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Text.Json; 4 | 5 | namespace QuickBooksSharp.Tests 6 | { 7 | [TestClass] 8 | public class WebhookEventTests 9 | { 10 | private readonly string _validNotification = 11 | @"{ 12 | ""eventNotifications"":[ 13 | { 14 | ""realmId"":""4620816365179422850"", 15 | ""dataChangeEvent"": { 16 | ""entities"": [ 17 | { 18 | ""name"":""Customer"", 19 | ""id"":""2"", 20 | ""operation"":""Update"", 21 | ""lastUpdated"":""2021-09-28T15:27:59.000Z"" 22 | }, 23 | { 24 | ""name"":""Invoice"", 25 | ""id"":""3"", 26 | ""operation"":""Void"", 27 | ""lastUpdated"":""2021-10-28T15:27:59.000Z"" 28 | }, 29 | { 30 | ""name"":""CreditMemo"", 31 | ""id"":""4"", 32 | ""operation"":""merge"", 33 | ""lastUpdated"":""2021-11-28T15:27:59.000Z"", 34 | ""deletedId"":""5"" 35 | } 36 | ] 37 | } 38 | } 39 | ] 40 | }"; 41 | 42 | [TestMethod] 43 | public void ShouldDeserializeValidJson() 44 | { 45 | WebhookEvent notification = JsonSerializer.Deserialize(_validNotification, QuickBooksHttpClient.JsonSerializerOptions); 46 | 47 | Assert.IsNotNull(notification); 48 | Assert.IsNotNull(notification.EventNotifications); 49 | Assert.IsTrue(notification.EventNotifications.Length > 0); 50 | Assert.IsNotNull(notification.EventNotifications[0]); 51 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent); 52 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent.Entities); 53 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities.Length > 0); 54 | // Customer 55 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent.Entities[0]); 56 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[0].Id == "2"); 57 | Assert.IsNull(notification.EventNotifications[0].DataChangeEvent.Entities[0].DeletedId); 58 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[0].Name == EntityChangedName.Customer); 59 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[0].Operation == Entities.OperationEnum.update); 60 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[0].LastUpdated == new DateTime(2021, 9, 28, 15, 27, 59, DateTimeKind.Utc)); 61 | // Invoice 62 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent.Entities[1]); 63 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[1].Id == "3"); 64 | Assert.IsNull(notification.EventNotifications[0].DataChangeEvent.Entities[1].DeletedId); 65 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[1].Name == EntityChangedName.Invoice); 66 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[1].Operation == Entities.OperationEnum.@void); 67 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[1].LastUpdated == new DateTime(2021, 10, 28, 15, 27, 59, DateTimeKind.Utc)); 68 | // CreditMemo 69 | Assert.IsNotNull(notification.EventNotifications[0].DataChangeEvent.Entities[2]); 70 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].Id == "4"); 71 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].DeletedId == "5"); 72 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].Name == EntityChangedName.CreditMemo); 73 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].Operation == Entities.OperationEnum.merge); 74 | Assert.IsTrue(notification.EventNotifications[0].DataChangeEvent.Entities[2].LastUpdated == new DateTime(2021, 11, 28, 15, 27, 59, DateTimeKind.Utc)); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /QuickBooksSharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31129.286 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickBooksSharp", "QuickBooksSharp\QuickBooksSharp.csproj", "{94291700-D916-46F5-A881-9B532241B466}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickBooksSharp.Tests", "QuickBooksSharp.Tests\QuickBooksSharp.Tests.csproj", "{1FB4D237-ED32-42FF-93CB-5AEE976A08A9}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{E347FD34-F1C5-4511-ABC5-3DD19D44A6B1}" 11 | ProjectSection(SolutionItems) = preProject 12 | .github\workflows\ci.yml = .github\workflows\ci.yml 13 | LICENSE = LICENSE 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickBooksSharp.CodeGen", "QuickBooksSharp.CodeGen\QuickBooksSharp.CodeGen.csproj", "{2941A980-49D2-4BA7-8A74-000C298A9497}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {94291700-D916-46F5-A881-9B532241B466}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {94291700-D916-46F5-A881-9B532241B466}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {94291700-D916-46F5-A881-9B532241B466}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {94291700-D916-46F5-A881-9B532241B466}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {1FB4D237-ED32-42FF-93CB-5AEE976A08A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {1FB4D237-ED32-42FF-93CB-5AEE976A08A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {1FB4D237-ED32-42FF-93CB-5AEE976A08A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {1FB4D237-ED32-42FF-93CB-5AEE976A08A9}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {2941A980-49D2-4BA7-8A74-000C298A9497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {2941A980-49D2-4BA7-8A74-000C298A9497}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {2941A980-49D2-4BA7-8A74-000C298A9497}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {2941A980-49D2-4BA7-8A74-000C298A9497}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(ExtensibilityGlobals) = postSolution 42 | SolutionGuid = {B8CDFCEB-F9EC-4C3E-9780-6915314200F5} 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /QuickBooksSharp/Authentication/AuthenticationService.cs: -------------------------------------------------------------------------------- 1 | using Flurl; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | 9 | namespace QuickBooksSharp 10 | { 11 | public class AuthenticationService : IAuthenticationService 12 | { 13 | private readonly QuickBooksHttpClient _client = new QuickBooksHttpClient(null, null, new NoRetryRunPolicy()); 14 | 15 | //TODO: retrieve the endpoints URLs dynamically 16 | //See https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-openid-discovery-doc 17 | 18 | private const string TOKEN_ENDPOINT_URL = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer"; 19 | private const string REVOKE_TOKEN_ENDPOINT_URL = "https://developer.api.intuit.com/v2/oauth2/tokens/revoke"; 20 | private const string USER_INFO_ENDPOINT_URL = "https://accounts.platform.intuit.com/v1/openid_connect/userinfo"; 21 | private const string USER_INFO_ENDPOINT_SANDBOX_URL = "https://sandbox-accounts.platform.intuit.com/v1/openid_connect/userinfo"; 22 | 23 | public string GenerateAuthorizationPromptUrl(string clientId, IEnumerable scopes, string redirectUrl, string state) 24 | { 25 | return new Url("https://appcenter.intuit.com/connect/oauth2") 26 | .SetQueryParam("client_id", clientId) 27 | .SetQueryParam("scope", string.Join(" ", scopes)) 28 | .SetQueryParam("redirect_uri", redirectUrl) 29 | .SetQueryParam("response_type", "code") 30 | .SetQueryParam("state", state) 31 | .ToString(); 32 | } 33 | 34 | public async Task GetUserInfo(string accessToken, bool useSandbox) 35 | { 36 | return await new QuickBooksHttpClient(accessToken, null, RunPolicy.DefaultRunPolicy).GetAsync(useSandbox ? USER_INFO_ENDPOINT_SANDBOX_URL : USER_INFO_ENDPOINT_URL); 37 | } 38 | 39 | public async Task GetOAuthTokenAsync(string clientId, string clientSecret, string code, string redirectUrl) 40 | { 41 | return await _client.SendAsync(() => 42 | { 43 | var request = new HttpRequestMessage(HttpMethod.Post, TOKEN_ENDPOINT_URL) 44 | { 45 | #pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. 46 | Content = new FormUrlEncodedContent(new Dictionary 47 | { 48 | { "code", code }, 49 | { "redirect_uri", redirectUrl }, 50 | { "grant_type", "authorization_code" }, 51 | }) 52 | #pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. 53 | }; 54 | this.AddAuthenticationHeader(request, clientId, clientSecret); 55 | return request; 56 | }); 57 | } 58 | 59 | /// 60 | /// When refreshed, the previous access token is invalidated immediately 61 | /// 62 | /// 63 | public async Task RefreshOAuthTokenAsync(string clientId, string clientSecret, string refreshToken) 64 | { 65 | return await _client.SendAsync(() => 66 | { 67 | var request = new HttpRequestMessage(HttpMethod.Post, TOKEN_ENDPOINT_URL) 68 | { 69 | #pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. 70 | Content = new FormUrlEncodedContent(new Dictionary 71 | { 72 | { "refresh_token", refreshToken }, 73 | { "grant_type", "refresh_token" }, 74 | }) 75 | #pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. 76 | }; 77 | this.AddAuthenticationHeader(request, clientId, clientSecret); 78 | return request; 79 | }); 80 | } 81 | 82 | public async Task RevokeOAuthTokenAsync(string clientId, string clientSecret, string tokenOrRefreshToken) 83 | { 84 | await _client.SendAsync(() => 85 | { 86 | var request = new HttpRequestMessage(HttpMethod.Post, REVOKE_TOKEN_ENDPOINT_URL) 87 | { 88 | Content = new StringContent(JsonSerializer.Serialize(new { token = tokenOrRefreshToken }), Encoding.UTF8, "application/json") 89 | }; 90 | this.AddAuthenticationHeader(request, clientId, clientSecret); 91 | return request; 92 | }); 93 | } 94 | 95 | private void AddAuthenticationHeader(HttpRequestMessage request, string clientId, string clientSecret) 96 | { 97 | request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"))); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /QuickBooksSharp/Authentication/IAuthenticationService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace QuickBooksSharp 5 | { 6 | public interface IAuthenticationService 7 | { 8 | /// 9 | /// Create the authorization request your app will send to the Intuit OAuth 2.0 Server. 10 | /// Request parameters should identify your app and include the required scopes. 11 | /// https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0#authorization-request 12 | /// 13 | /// Identifies the app making the request. 14 | /// Lists the scopes your app uses. The list is space-delimited. 15 | /// Enter one or more scopes.The scope value defines the apps’ access and type of data it can utilize.This information appears on the authorization page when end-users connect to your app. 16 | /// Tip: We recommend apps request scopes incrementally based on your feature requirements, rather than every scope up front. 17 | /// Determines where the Intuit OAuth 2.0 Server redirects users after they grant permission to your app. 18 | /// The redirect value must match the URI you listed in Step 6 exactly.That includes the casing, http scheme, and trailing “/.” 19 | /// Defines the state between your authorization request and the Intuit OAuth 2.0 Server response. 20 | /// The purpose of the state field is to validate if the client (i.e.your app) gets back what was sent in the original request.Thus, the state is maintained from send to response. 21 | /// You can enter any string value to maintain the state. The server should return the exact name:value pair you sent in the original request. 22 | /// Tip: We strongly recommend you include an anti-forgery token for the state and confirm it in the response This prevents cross-site request forgery.Learn more about CSRF. 23 | /// Url for authorization request 24 | string GenerateAuthorizationPromptUrl(string clientId, IEnumerable scopes, string redirectUrl, string state); 25 | /// 26 | /// At this point, your app is waiting for a response from the Intuit OAuth 2.0 Server. If users approve access, the Intuit OAuth 2.0 Server sends a response to the redirect URI you specified. 27 | /// 28 | /// The client id of your QuickBooks application making the request 29 | /// The client secret of your QuickBooks application 30 | /// The authorization code sent by the Intuit OAuth 2.0 Server. Max length: 512 characters 31 | /// Determines where the Intuit OAuth 2.0 Server redirects users after they grant permission to your app. 32 | /// The redirect value must match the URI you listed in Step 6 exactly.That includes the casing, http scheme, and trailing “/.” 33 | /// 34 | Task GetOAuthTokenAsync(string clientId, string clientSecret, string code, string redirectUrl); 35 | /// 36 | /// Obtains User information 37 | /// 38 | /// The token used to access our API. Max length: 4096 characters. 39 | /// Determines if Sandbox environment is used 40 | /// 41 | Task GetUserInfo(string accessToken, bool useSandbox); 42 | /// 43 | /// Access tokens eventually expire. Use refresh tokens to “refresh” expired access tokens. You can refresh access tokens without prompting users for permission. 44 | /// 45 | /// The client id of your QuickBooks application making the request 46 | /// The client secret of your QuickBooks application 47 | /// The last refresh token obtained from either or 48 | /// 49 | Task RefreshOAuthTokenAsync(string clientId, string clientSecret, string refreshToken); 50 | /// 51 | /// If users need to disconnect from your app, you need a way to automatically revoke access. You can use the sample code below as a model. 52 | /// 53 | /// The client id of your QuickBooks application making the request 54 | /// The client secret of your QuickBooks application 55 | /// The last access or refresh token obtained from either or 56 | /// 57 | Task RevokeOAuthTokenAsync(string clientId, string clientSecret, string tokenOrRefreshToken); 58 | } 59 | } -------------------------------------------------------------------------------- /QuickBooksSharp/Authentication/RevokeTokenRequest.cs: -------------------------------------------------------------------------------- 1 | namespace QuickBooksSharp 2 | { 3 | public class RevokeTokenRequest 4 | { 5 | /// 6 | /// token or refresh_token 7 | /// 8 | public string token { get; set; } = default!; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /QuickBooksSharp/Authentication/TokenRequest.cs: -------------------------------------------------------------------------------- 1 | namespace QuickBooksSharp 2 | { 3 | public class TokenRequest 4 | { 5 | public string code { get; set; } = default!; 6 | 7 | public string redirect_uri { get; set; } = default!; 8 | 9 | /// 10 | /// "authorization_code" or "refresh_token" 11 | /// 12 | public string grant_type { get; set; } = default!; 13 | 14 | public string refresh_token { get; set; } = default!; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /QuickBooksSharp/Authentication/TokenResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace QuickBooksSharp 7 | { 8 | public class TokenResponse 9 | { 10 | /// 11 | /// "bearer" 12 | /// 13 | public string token_type { get; set; } = default!; 14 | 15 | public string access_token { get; set; } = default!; 16 | 17 | public string refresh_token { get; set; } = default!; 18 | 19 | /// 20 | /// duration in seconds 21 | /// 22 | [JsonConverter(typeof(NumberTimespanConverter))] 23 | public TimeSpan expires_in { get; set; } 24 | 25 | /// 26 | /// duration in seconds 27 | /// 28 | [JsonConverter(typeof(NumberTimespanConverter))] 29 | public TimeSpan x_refresh_token_expires_in { get; set; } 30 | 31 | public string? id_token { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /QuickBooksSharp/Authentication/UserInfo.cs: -------------------------------------------------------------------------------- 1 | namespace QuickBooksSharp 2 | { 3 | public class UserInfo 4 | { 5 | #pragma warning disable CS8618 6 | public string sub { get; set; } 7 | #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 8 | 9 | public string? email { get; set; } 10 | 11 | public bool? emailVerified { get; set; } 12 | 13 | public string? givenName { get; set; } 14 | 15 | public string? familyName { get; set; } 16 | 17 | public string? phoneNumber { get; set; } 18 | 19 | public bool? phoneNumberVerified { get; set; } 20 | 21 | public UserAddress? address { get; set; } 22 | 23 | public class UserAddress 24 | { 25 | public string? streetAddress { get; set; } 26 | 27 | public string? locality { get; set; } 28 | 29 | public string? region { get; set; } 30 | 31 | public string? postalCode { get; set; } 32 | 33 | public string? country { get; set; } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /QuickBooksSharp/Entities/IntuitResponseOfT.cs: -------------------------------------------------------------------------------- 1 | using QuickBooksSharp.Entities; 2 | using System; 3 | 4 | namespace QuickBooksSharp 5 | { 6 | public class IntuitResponse where TResponse : class 7 | { 8 | /// 9 | /// Indication that a request was processed, but with possible exceptional circumstances (i.e. ignored unsupported fields) that the client may want to be aware of 10 | /// 11 | public Warnings? Warnings { get; set; } 12 | 13 | /// 14 | /// RequestId associated with the request 15 | /// 16 | public string? RequestId { get; set; } 17 | 18 | /// 19 | /// Time at which request started processing in the server 20 | /// 21 | public DateTimeOffset? Time { get; set; } 22 | 23 | /// 24 | /// HTTP codes result of the operation< 25 | /// 26 | public string? Status { get; set; } 27 | 28 | public Fault? Fault { get; set; } 29 | 30 | public TResponse? Response { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /QuickBooksSharp/Entities/QueryCountResponse.cs: -------------------------------------------------------------------------------- 1 | using QuickBooksSharp.Entities; 2 | 3 | namespace QuickBooksSharp 4 | { 5 | public class QueryCountResponse 6 | { 7 | public Warnings? Warnings { get; set; } 8 | 9 | public int? TotalCount { get; set; } 10 | 11 | public Fault? Fault { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QuickBooksSharp/Entities/QueryResponseOfT.cs: -------------------------------------------------------------------------------- 1 | using QuickBooksSharp.Entities; 2 | 3 | namespace QuickBooksSharp 4 | { 5 | public class QueryResponse where TEntity : IntuitEntity 6 | { 7 | public Warnings? Warnings { get; set; } 8 | 9 | public int? StartPosition { get; set; } 10 | 11 | public int? MaxResults { get; set; } 12 | 13 | public int? TotalCount { get; set; } 14 | 15 | public Fault? Fault { get; set; } 16 | 17 | public TEntity[]? Entities { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /QuickBooksSharp/Helper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | using System.Text.Json; 5 | 6 | namespace QuickBooksSharp 7 | { 8 | public class Helper 9 | { 10 | /// 11 | /// Returns whether the webhook request was signed by Intui 12 | /// 13 | /// Value from the HTTP header "intuit-signature" 14 | /// The webhook verifier token located in the Intuit Developer dashboard 15 | /// The full HTTP requests body 16 | /// 17 | public static bool IsAuthenticWebhook(string intuitSignature, string webhookVerifierToken, string requestBody) 18 | { 19 | var verifierTokenBytes = Encoding.UTF8.GetBytes(webhookVerifierToken); 20 | var jsonBytes = Encoding.UTF8.GetBytes(requestBody); 21 | var hmac = new HMACSHA256(verifierTokenBytes); 22 | var hmacBytes = hmac.ComputeHash(jsonBytes); 23 | var hash = Convert.ToBase64String(hmacBytes); 24 | return hash == intuitSignature; 25 | } 26 | 27 | public static string SerializeToJSON(object o) 28 | { 29 | return JsonSerializer.Serialize(o, QuickBooksHttpClient.JsonSerializerOptions); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /QuickBooksSharp/Infrastructure/IQuickBooksHttpClient.cs: -------------------------------------------------------------------------------- 1 | using Flurl; 2 | using System; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace QuickBooksSharp 7 | { 8 | public interface IQuickBooksHttpClient 9 | { 10 | Task GetAsync(Url url); 11 | Task PostAsync(Url url, object content); 12 | Task SendAsync(Func makeRequest); 13 | Task SendAsync(Func makeRequest); 14 | } 15 | } -------------------------------------------------------------------------------- /QuickBooksSharp/Infrastructure/NumberTimespanConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace QuickBooksSharp 6 | { 7 | public class NumberTimespanConverter : JsonConverter 8 | { 9 | public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | return TimeSpan.FromSeconds(reader.GetInt32()); 12 | } 13 | 14 | public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) 15 | { 16 | writer.WriteNumberValue((int)value.TotalSeconds); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /QuickBooksSharp/Infrastructure/QuickBooksException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | 5 | namespace QuickBooksSharp 6 | { 7 | public class QuickBooksException : Exception 8 | { 9 | public HttpRequestMessage Request { get; } 10 | 11 | public HttpResponseMessage Response { get; } 12 | 13 | public string ResponseContent { get; } 14 | 15 | /// 16 | /// HTTP 401 17 | /// 18 | public bool IsUnauthorized => (int)Response.StatusCode == 401; 19 | 20 | /// 21 | /// HTTP 403 22 | /// 23 | public bool IsForbidden => (int)Response.StatusCode == 403; 24 | 25 | /// 26 | /// HTTP 429 27 | /// 28 | public bool IsRateLimit => (int)Response.StatusCode == 429; 29 | 30 | public string? IntuitTId => GetHeaderValue(Response, "intuit_tid"); 31 | 32 | public string? QBOVersion => GetHeaderValue(Response, "QBO-Version"); 33 | 34 | public string? ErrorCode => GetHeaderValue(Response, "ErrorCode"); 35 | 36 | public string? ErrorCause => GetHeaderValue(Response, "ErrorCause"); 37 | 38 | private static string? GetHeaderValue(HttpResponseMessage r, string headerName) => r.Headers.TryGetValues(headerName, out var values) ? values.FirstOrDefault() : null; 39 | 40 | public QuickBooksException(HttpRequestMessage request, HttpResponseMessage response, string responseContent) 41 | : base($@"QuickBooks API call to {request.RequestUri} failed with code: {response.StatusCode} 42 | IntuiTId: {GetHeaderValue(response, "intuit_tid")}, 43 | Reason: {response.ReasonPhrase} 44 | Content: {responseContent}") 45 | { 46 | this.Request = request; 47 | this.Response = response; 48 | this.ResponseContent = responseContent; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /QuickBooksSharp/Infrastructure/QuickBooksHttpClient.cs: -------------------------------------------------------------------------------- 1 | using Flurl; 2 | using System; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Net.Http.Json; 7 | using System.Text; 8 | using System.Text.Json; 9 | using System.Text.Json.Serialization; 10 | using System.Threading.Tasks; 11 | 12 | namespace QuickBooksSharp 13 | { 14 | public class QuickBooksHttpClient : IQuickBooksHttpClient 15 | { 16 | private readonly string? _accessToken; 17 | private readonly long? _realmId; 18 | private IRunPolicy _runPolicy; 19 | 20 | private static HttpClient _httpClient = new HttpClient(new HttpClientHandler 21 | { 22 | AutomaticDecompression = DecompressionMethods.GZip 23 | }); 24 | 25 | 26 | public readonly static JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions 27 | { 28 | Converters = 29 | { 30 | //new JsonStringEnumConverter(), 31 | 32 | //using community package to fix https://github.com/dotnet/runtime/issues/31081 33 | //can revert to out of the box converter once fix (.net 6?) 34 | new JsonStringEnumMemberConverter() 35 | } 36 | }; 37 | 38 | static QuickBooksHttpClient() 39 | { 40 | _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(nameof(QuickBooksSharp), typeof(QuickBooksHttpClient).Assembly.GetName().Version!.ToString())); 41 | _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("(github.com/better-reports/QuickBooksSharp)")); 42 | _httpClient.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); 43 | _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 44 | } 45 | 46 | public QuickBooksHttpClient(string? accessToken, long? realmId, IRunPolicy runPolicy) 47 | { 48 | _accessToken = accessToken; 49 | _realmId = realmId; 50 | _runPolicy = runPolicy; 51 | } 52 | 53 | public async Task GetAsync(Url url) 54 | { 55 | Func makeRequest = () => new HttpRequestMessage(HttpMethod.Get, url); 56 | return await this.SendAsync(makeRequest); 57 | } 58 | 59 | public async Task PostAsync(Url url, object content) 60 | { 61 | Func makeRequest = () => new HttpRequestMessage(HttpMethod.Post, url) 62 | { 63 | Content = new StringContent(JsonSerializer.Serialize(content, JsonSerializerOptions), Encoding.UTF8, "application/json") 64 | }; 65 | return await this.SendAsync(makeRequest); 66 | } 67 | 68 | public async Task SendAsync(Func makeRequest) 69 | { 70 | var response = await this.SendAsync(makeRequest); 71 | return (await response.Content.ReadFromJsonAsync(JsonSerializerOptions))!; 72 | } 73 | 74 | public async Task SendAsync(Func makeRequest) 75 | { 76 | var response = await this._runPolicy.RunAsync(_realmId, async () => 77 | { 78 | using (var request = makeRequest()) 79 | { 80 | if (_accessToken != null) 81 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); 82 | 83 | var response = await _httpClient.SendAsync(request); 84 | var ex = response.IsSuccessStatusCode ? null : new QuickBooksException(request, response, await response.Content.ReadAsStringAsync()); 85 | 86 | if (ex?.IsRateLimit == true) 87 | RunPolicy.NotifyRateLimt(new RateLimitEvent(_realmId, request.RequestUri)); 88 | 89 | return new QuickBooksAPIResponse(response, ex); 90 | } 91 | }); 92 | 93 | return response; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /QuickBooksSharp/Infrastructure/QuickBooksUrl.cs: -------------------------------------------------------------------------------- 1 | using Flurl; 2 | 3 | namespace QuickBooksSharp 4 | { 5 | /// 6 | /// Creates the BaseUrl for QuickBooks Online API 7 | /// 8 | public static class QuickBooksUrl 9 | { 10 | public const string SandboxBaseUrl = "https://sandbox-quickbooks.api.intuit.com"; 11 | public const string ProductionBaseUrl = "https://quickbooks.api.intuit.com"; 12 | 13 | public const int MajorVersion = 3; 14 | 15 | public const int MinorVersion = 75; 16 | 17 | public static readonly string Version = $"{MajorVersion}.{MinorVersion}"; 18 | 19 | /// 20 | /// Creates the using the and specified. 21 | /// 22 | /// Determines if the Sandbox or Production url will be used. 23 | /// The realm ID is a unique ID value which identifies a specific QuickBooks Online company 24 | /// 25 | /// Creates a instance with: 26 | /// Sandbox: $"{}/v3/company/{}?minorversion=}" 27 | /// Production: $"{}/v3/company/{}?minorversion=}" 28 | /// 29 | public static Url Build(bool useSandbox, long realmId) 30 | { 31 | var serviceBaseUrl = useSandbox ? SandboxBaseUrl : ProductionBaseUrl; 32 | 33 | return new Url($"{serviceBaseUrl}/v{MajorVersion}/company/{realmId}") 34 | .SetQueryParam("minorversion", MinorVersion); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /QuickBooksSharp/Policies/FifoSemaphore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace QuickBooksSharp 6 | { 7 | internal class FifoSemaphore 8 | { 9 | private SemaphoreSlim _semaphore; 10 | 11 | private ConcurrentQueue> queue = new ConcurrentQueue>(); 12 | 13 | public int QueueCount => queue.Count; 14 | 15 | public FifoSemaphore(int maxConcurrency) 16 | { 17 | _semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency); 18 | } 19 | 20 | public Task WaitAsync() 21 | { 22 | var tcs = new TaskCompletionSource(); 23 | queue.Enqueue(tcs); 24 | _semaphore.WaitAsync().ContinueWith(t => 25 | { 26 | if (queue.TryDequeue(out var popped)) 27 | popped.SetResult(true); 28 | }); 29 | return tcs.Task; 30 | } 31 | 32 | public void Release() 33 | { 34 | _semaphore.Release(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /QuickBooksSharp/Policies/IRunPolicy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace QuickBooksSharp 6 | { 7 | public interface IRunPolicy 8 | { 9 | //we use a request factory to make a new request when a retry is needed 10 | //that is because request message cannot be reused 11 | Task RunAsync(long? realmId, Func> getResponseAsync); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /QuickBooksSharp/Policies/MaxConcurrencyRetryRunPolicy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace QuickBooksSharp 7 | { 8 | public class MaxConcurrencyRetryRunPolicy : IRunPolicy 9 | { 10 | //QBO API allows up to 10 concurrent requests 11 | //https://developer.intuit.com/app/developer/qbo/docs/learn/rest-api-features#limits-and-throttles 12 | private const int MAX_CONCURRENT_REQUESTS = 10; 13 | 14 | private ConcurrentDictionary _realmIdToQueue = new ConcurrentDictionary(); 15 | 16 | public int GetQueueCount(long realmId) => _realmIdToQueue[realmId].QueueCount; 17 | 18 | public async Task RunAsync(long? realmId, Func> getResponseAsync) 19 | { 20 | FifoSemaphore? sem = null; 21 | 22 | if (realmId != null) 23 | { 24 | sem = _realmIdToQueue.GetOrAdd(realmId.Value, _ => new FifoSemaphore(MAX_CONCURRENT_REQUESTS)); 25 | await sem.WaitAsync(); 26 | } 27 | 28 | try 29 | { 30 | return await RunCoreAsync(getResponseAsync); 31 | } 32 | finally 33 | { 34 | sem?.Release(); 35 | } 36 | } 37 | 38 | private async Task RunCoreAsync(Func> getResponseAsync) 39 | { 40 | while (true) 41 | { 42 | var r = await getResponseAsync(); 43 | 44 | if (r.Exception?.IsRateLimit == true) 45 | { 46 | await Task.Delay(TimeSpan.FromSeconds(5)); 47 | continue; 48 | } 49 | 50 | if (r.Exception != null) 51 | throw r.Exception; 52 | 53 | return r.Response; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /QuickBooksSharp/Policies/NoRetryRunPolicy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace QuickBooksSharp 6 | { 7 | public class NoRetryRunPolicy : IRunPolicy 8 | { 9 | public async Task RunAsync(long? realmId, Func> getResponseAsync) 10 | { 11 | var r = await getResponseAsync(); 12 | 13 | if (r.Exception != null) 14 | throw r.Exception; 15 | 16 | return r.Response; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /QuickBooksSharp/Policies/QuickBooksAPIResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace QuickBooksSharp 4 | { 5 | public class QuickBooksAPIResponse 6 | { 7 | internal HttpResponseMessage Response { get; private set; } 8 | 9 | internal QuickBooksException? Exception { get; private set; } 10 | 11 | public QuickBooksAPIResponse(HttpResponseMessage response, QuickBooksException? ex) 12 | { 13 | Response = response; 14 | Exception = ex; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /QuickBooksSharp/Policies/RateLimitEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace QuickBooksSharp 4 | { 5 | public class RateLimitEvent 6 | { 7 | public long? RealmId { get; } 8 | 9 | public Uri RequestUri { get; } 10 | 11 | public RateLimitEvent(long? realmId, Uri requestUri) 12 | { 13 | RealmId = realmId; 14 | RequestUri = requestUri; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /QuickBooksSharp/Policies/RunPolicy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Subjects; 4 | 5 | namespace QuickBooksSharp 6 | { 7 | public static class RunPolicy 8 | { 9 | public static IRunPolicy DefaultRunPolicy { get; set; } = new MaxConcurrencyRetryRunPolicy(); 10 | 11 | internal static ISubject _RateLimitFired = new Subject(); 12 | 13 | public static readonly IObservable RateLimitFired = _RateLimitFired.AsObservable(); 14 | 15 | internal static void NotifyRateLimt(RateLimitEvent rateLimitEvent) 16 | { 17 | _RateLimitFired.OnNext(rateLimitEvent); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /QuickBooksSharp/Policies/SimpleRetryRunPolicy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace QuickBooksSharp 6 | { 7 | public class SimpleRetryRunPolicy : IRunPolicy 8 | { 9 | public async Task RunAsync(long? realmId, Func> getResponseAsync) 10 | { 11 | while (true) 12 | { 13 | var r = await getResponseAsync(); 14 | 15 | if (r.Exception?.IsRateLimit == true) 16 | { 17 | await Task.Delay(TimeSpan.FromSeconds(5)); 18 | continue; 19 | } 20 | 21 | if (r.Exception != null) 22 | throw r.Exception; 23 | 24 | return r.Response; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /QuickBooksSharp/QuickBooksSharp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net8.0;net9.0 5 | Better Reports 6 | Better Reports 7 | 8 | true 9 | https://github.com/better-reports/QuickBooksSharp 10 | https://github.com/better-reports/QuickBooksSharp 11 | Modern .NET Client for QuickBooks Online API 12 | 3.5.0 13 | enable 14 | quickbooks online intuit qbo accounting 15 | DateOnly properties generated for .NET6+ 16 | latest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /QuickBooksSharp/Services/DataService.cs: -------------------------------------------------------------------------------- 1 | using Flurl; 2 | using QuickBooksSharp.Entities; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Net.Http.Headers; 9 | using System.Threading.Tasks; 10 | 11 | namespace QuickBooksSharp 12 | { 13 | public class DataService : IDataService 14 | { 15 | protected readonly QuickBooksHttpClient _client; 16 | 17 | protected readonly Url _serviceUrl; 18 | 19 | public DataService(string accessToken, long realmId, bool useSandbox, IRunPolicy? runPolicy = null) 20 | { 21 | _client = new QuickBooksHttpClient(accessToken, realmId, runPolicy ?? RunPolicy.DefaultRunPolicy); 22 | _serviceUrl = QuickBooksUrl.Build(useSandbox, realmId); 23 | } 24 | 25 | public async Task> QueryCountAsync(string queryCount) 26 | { 27 | var res = await QueryAsync(queryCount); 28 | return new IntuitResponse 29 | { 30 | RequestId = res.RequestId, 31 | Time = res.Time, 32 | Status = res.Status, 33 | Warnings = res.Warnings, 34 | Fault = res.Fault, 35 | Response = new QueryCountResponse 36 | { 37 | Fault = res.Response?.Fault, 38 | Warnings = res.Response?.Warnings, 39 | TotalCount = res.Response?.TotalCount 40 | } 41 | }; 42 | } 43 | 44 | public async Task>> QueryAsync(string query) where TEntity : IntuitEntity 45 | { 46 | var res = await _client.GetAsync(new Url(_serviceUrl).AppendPathSegment("query") 47 | .SetQueryParam("query", query)); 48 | var queryRes = res.QueryResponse; 49 | return new IntuitResponse> 50 | { 51 | RequestId = res.requestId, 52 | Time = res.time, 53 | Status = res.status, 54 | Warnings = res.Warnings, 55 | Fault = res.Fault, 56 | Response = new QueryResponse 57 | { 58 | MaxResults = queryRes?.maxResults, 59 | StartPosition = queryRes?.startPosition, 60 | TotalCount = queryRes?.totalCount, 61 | Warnings = queryRes?.Warnings, 62 | Fault = queryRes?.Fault, 63 | Entities = queryRes?.IntuitObjects?.Cast().ToArray() 64 | } 65 | }; 66 | } 67 | 68 | public async Task> GetAsync(string id) where TEntity : IntuitEntity 69 | { 70 | var res = await _client.GetAsync(new Url(_serviceUrl).AppendPathSegment(GetEntityName(typeof(TEntity))) 71 | .AppendPathSegment(id)); 72 | return new IntuitResponse 73 | { 74 | RequestId = res.requestId, 75 | Time = res.time, 76 | Status = res.status, 77 | Warnings = res.Warnings, 78 | Fault = res.Fault, 79 | Response = (TEntity?)res.IntuitObject 80 | }; 81 | } 82 | 83 | public async Task> GetAsync(string id, Type entityType) 84 | { 85 | var res = await _client.GetAsync(new Url(_serviceUrl).AppendPathSegment(GetEntityName(entityType)) 86 | .AppendPathSegment(id)); 87 | return new IntuitResponse 88 | { 89 | RequestId = res.requestId, 90 | Time = res.time, 91 | Status = res.status, 92 | Warnings = res.Warnings, 93 | Fault = res.Fault, 94 | Response = res.IntuitObject 95 | }; 96 | } 97 | 98 | public async Task GetReportAsync(string reportName, Dictionary parameters) 99 | { 100 | var url = new Url(_serviceUrl).AppendPathSegment($"reports/{reportName}"); 101 | foreach (var p in parameters) 102 | url.SetQueryParam(p.Key, p.Value); 103 | return await _client.GetAsync(url); 104 | } 105 | 106 | public async Task> GetCDCAsync(DateTimeOffset changedSince, IEnumerable entityNames) 107 | { 108 | var url = new Url(_serviceUrl).AppendPathSegment($"cdc") 109 | .SetQueryParam("changedSince", changedSince) 110 | .SetQueryParam("entities", string.Join(",", entityNames)); 111 | var res = await _client.GetAsync(url); 112 | return new IntuitResponse 113 | { 114 | RequestId = res.requestId, 115 | Time = res.time, 116 | Status = res.status, 117 | Warnings = res.Warnings, 118 | Fault = res.Fault, 119 | 120 | //https://help.developer.intuit.com/s/question/0D54R00007pjGeCSAU/in-the-xsd-why-is-intuitresponsecdcresponse-maxoccurs-unbounded 121 | Response = res.CDCResponse![0] //schema returns array but should be single => REPORT ERROR TO QB FORUM 122 | }; 123 | } 124 | 125 | public async Task> BatchAsync(IntuitBatchRequest r) 126 | { 127 | var res = await _client.PostAsync(new Url(_serviceUrl).AppendPathSegment("batch"), r); 128 | return new IntuitResponse 129 | { 130 | RequestId = res.requestId, 131 | Time = res.time, 132 | Status = res.status, 133 | Warnings = res.Warnings, 134 | Fault = res.Fault, 135 | Response = res.BatchItemResponse 136 | }; 137 | } 138 | 139 | /// 140 | public async Task> PostAsync(TEntity e, OperationEnum? operation = null, OperationEnum? include = null) where TEntity : IntuitEntity 141 | { 142 | var url = new Url(_serviceUrl).AppendPathSegment(GetEntityName(typeof(TEntity))); 143 | 144 | if (operation != null && operation != OperationEnum.Unspecified) 145 | url = url.SetQueryParam("operation", operation); 146 | 147 | if (include != null && include != OperationEnum.Unspecified) 148 | url = url.SetQueryParam("include", include); 149 | 150 | var res = await _client.PostAsync(url, e); 151 | return new IntuitResponse 152 | { 153 | RequestId = res.requestId, 154 | Time = res.time, 155 | Status = res.status, 156 | Warnings = res.Warnings, 157 | Fault = res.Fault, 158 | Response = (TEntity?)res.IntuitObject 159 | }; 160 | } 161 | 162 | private async Task PostWithEntityResultAsync(TEntity e) 163 | { 164 | var url = new Url(_serviceUrl).AppendPathSegment(GetEntityName(typeof(TaxService))); 165 | 166 | return await _client.PostAsync(url, e); 167 | } 168 | 169 | /// 170 | /// Unlike other entities, TaxService is a special case where the return type is not an IntuitResponse but the entity itself. 171 | /// 172 | public async Task PostTaxServiceAsync(TaxService taxService) 173 | { 174 | return await PostWithEntityResultAsync(taxService); 175 | } 176 | 177 | /// 178 | public async Task GetInvoicePDFAsync(string invoiceId) 179 | { 180 | var url = new Url(_serviceUrl).AppendPathSegment($"/invoice/{invoiceId}/pdf"); 181 | var res = await _client.SendAsync(() => 182 | { 183 | var httpRequest = new HttpRequestMessage(HttpMethod.Get, url); 184 | httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/pdf")); 185 | return httpRequest; 186 | }); 187 | 188 | return await res.Content.ReadAsStreamAsync(); 189 | } 190 | 191 | private string GetEntityName(Type t) 192 | { 193 | if (t == typeof(CreditCardPaymentTxn)) 194 | return "creditcardpayment"; 195 | else if (t == typeof(TaxService)) 196 | return "taxservice/taxcode"; 197 | 198 | return t.Name.ToLowerInvariant(); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /QuickBooksSharp/Services/IDataService.cs: -------------------------------------------------------------------------------- 1 | using QuickBooksSharp.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | 7 | namespace QuickBooksSharp 8 | { 9 | public interface IDataService 10 | { 11 | Task> BatchAsync(IntuitBatchRequest r); 12 | Task> GetAsync(string id, Type entityType); 13 | Task> GetAsync(string id) where TEntity : IntuitEntity; 14 | Task> GetCDCAsync(DateTimeOffset changedSince, IEnumerable entityNames); 15 | Task GetReportAsync(string reportName, Dictionary parameters); 16 | 17 | /// 18 | /// Create, Update, or SparseUpdate the entity, depending on the value of the 'sparse' property 19 | /// 20 | /// QuickBooks Entity 21 | /// Entity to be sent 22 | /// Defines the operation to be executed in QuickBooks. 23 | /// Defines the "include" query parameter. For example: Required when voiding a payment 24 | /// 25 | Task> PostAsync(TEntity e, OperationEnum? operation = null, OperationEnum? include = null) where TEntity : IntuitEntity; 26 | 27 | Task>> QueryAsync(string query) where TEntity : IntuitEntity; 28 | 29 | /// 30 | /// Get an invoice as PDF 31 | /// This resource returns the specified object in the response body as an Adobe Portable Document Format (PDF) file. The resulting PDF file is formatted according to custom form styles in the company settings. 32 | /// QBO Documentation 33 | /// 34 | /// Unique identifier for this object 35 | /// This resource returns the specified object in the response body as an Adobe Portable Document Format (PDF) file. The resulting PDF file is formatted according to custom form styles in the company settings. 36 | Task GetInvoicePDFAsync(string invoiceId); 37 | 38 | Task PostTaxServiceAsync(TaxService taxService); 39 | } 40 | } -------------------------------------------------------------------------------- /QuickBooksSharp/Webhooks/DataChangeEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QuickBooksSharp 4 | { 5 | public class DataChangeEvent 6 | { 7 | [JsonPropertyName("entities")] 8 | public EntityChange[] Entities { get; set; } = default!; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /QuickBooksSharp/Webhooks/EntityChange.cs: -------------------------------------------------------------------------------- 1 | using QuickBooksSharp.Entities; 2 | using System; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace QuickBooksSharp 6 | { 7 | /// 8 | /// Information about the entity that changed (customer, Invoice, etc.) 9 | /// 10 | public class EntityChange 11 | { 12 | /// 13 | /// The name of the entity that changed (customer, Invoice, etc.) 14 | /// 15 | [JsonPropertyName("name")] 16 | public EntityChangedName Name { get; set; } 17 | /// 18 | /// The ID of the changed entity 19 | /// 20 | [JsonPropertyName("id")] 21 | public string Id { get; set; } = default!; 22 | /// 23 | /// The type of change 24 | /// 25 | [JsonPropertyName("operation")] 26 | public OperationEnum Operation { get; set; } 27 | /// 28 | /// The latest timestamp in UTC 29 | /// 30 | [JsonPropertyName("lastUpdated")] 31 | public DateTime LastUpdated { get; set; } 32 | /// 33 | /// The ID of the deleted or merged entity (this only applies to merge events) 34 | /// 35 | [JsonPropertyName("deletedId")] 36 | public string? DeletedId { get; set; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /QuickBooksSharp/Webhooks/EntityChangedName.cs: -------------------------------------------------------------------------------- 1 | namespace QuickBooksSharp 2 | { 3 | /// 4 | /// Supported API entities 5 | /// 6 | /// https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks#sample-implementations 7 | /// 8 | public enum EntityChangedName 9 | { 10 | Account, 11 | Bill, 12 | BillPayment, 13 | Budget, 14 | Class, 15 | CreditMemo, 16 | Currency, 17 | Customer, 18 | Department, 19 | Deposit, 20 | Employee, 21 | Estimate, 22 | Invoice, 23 | Item, 24 | JournalCode, 25 | JournalEntry, 26 | Payment, 27 | PaymentMethod, 28 | Preferences, 29 | Purchase, 30 | PurchaseOrder, 31 | RefundReceipt, 32 | SalesReceipt, 33 | TaxAgency, 34 | Term, 35 | TimeActivity, 36 | Transfer, 37 | Vendor, 38 | VendorCredit 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /QuickBooksSharp/Webhooks/EventNotification.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace QuickBooksSharp 4 | { 5 | public class EventNotification 6 | { 7 | [JsonPropertyName("realmId")] 8 | public string RealmId { get; set; } = default!; 9 | [JsonPropertyName("dataChangeEvent")] 10 | public DataChangeEvent DataChangeEvent { get; set; } = default!; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /QuickBooksSharp/Webhooks/WebhookEvent.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace QuickBooksSharp 6 | { 7 | /// 8 | /// Webhooks are designed to handle updates for multiple QuickBooks Online companies (specified by the realm ID). 9 | /// However, each notification is tied to a single realm ID.If your app is connected to multiple QuickBooks Online companies, you’ll get individual webhook notifications for each company (i.e.one eventNotification per realm ID). 10 | /// 11 | /// Documentation: https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks 12 | /// 13 | /// 14 | /// There are no Intuit-imposed limits to payload size or number of events. Individual server architectures may impose their own limits (2MB is a common default size limit). Assume this limit is imposed by your server unless you know otherwise. 15 | /// 16 | public class WebhookEvent 17 | { 18 | [JsonPropertyName("eventNotifications")] 19 | public EventNotification[] EventNotifications { get; set; } = default!; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickBooksSharp 2 | 3 | Modern, async first, .NET client for QuickBooks Online Accounting API 4 | 5 | This project was created because the [official .NET client](https://github.com/intuit/QuickBooks-V3-DotNET-SDK) provided by Intuit is hard to work with and based on outdated .NET patterns. 6 | 7 | https://developer.intuit.com/app/developer/qbo/docs/develop 8 | 9 | API version: 3.75 10 | 11 | ![Build](https://github.com/better-reports/QuickBooksSharp/actions/workflows/ci.yml/badge.svg) 12 | 13 | https://www.nuget.org/packages/QuickBooksSharp/ 14 | 15 | `Install-Package QuickBooksSharp` 16 | 17 | ## OAuth authentication 18 | 19 | ### Generate URL to redirect user for approval of connection: 20 | ```csharp 21 | var authService = new AuthenticationService(); 22 | var scopes = new[] { "com.intuit.quickbooks.accounting" }; 23 | string redirectUrl = "https://myapp.com/quickbooks/authresult"; 24 | string state = Guid.NewGuid().ToString(); 25 | string authUrl = authService.GenerateAuthorizationPromptUrl(clientId, scopes, redirectUrl, state); 26 | // Redirect the user to authUrl so that they can approve the connection 27 | ``` 28 | 29 | ### Exchange code for token 30 | ```csharp 31 | [HttpGet] 32 | public async Task AuthResult(string code, long realmId, string state) 33 | { 34 | //validate state parameter 35 | var authService = new AuthenticationService(); 36 | string clientId = //get from config 37 | string clientSecret = //get from config 38 | var result = await authService.GetOAuthTokenAsync(clientId, clientSecret, code, redirectUrl); 39 | //persit access token and refresh token 40 | ... 41 | } 42 | ``` 43 | 44 | ### Refresh token 45 | ```csharp 46 | var authService = new AuthenticationService(); 47 | var result = await authService.RefreshOAuthTokenAsync(clientId, clientSecret, refreshToken); 48 | //persit access token and refresh token 49 | ``` 50 | 51 | ### Get User Info 52 | ```csharp 53 | var authService = new AuthenticationService(); 54 | var userInfo = await authService.GetUserInfo(accessToken, useSandbox: true); 55 | //persit access token and refresh token 56 | ``` 57 | 58 | ## Instantiating the DataService 59 | ```csharp 60 | var dataService = new DataService(accessToken, realmId, useSandbox: true); 61 | ``` 62 | 63 | ## Creating / Updating entities 64 | ```csharp 65 | var result = await dataService.PostAsync(new Customer 66 | { 67 | DisplayName = "Chandler Bing", 68 | Suffix = "Jr", 69 | Title = "Mr", 70 | MiddleName = "Muriel", 71 | FamilyName = "Bing", 72 | GivenName = "Chandler", 73 | }); 74 | //result.Response is of type Customer 75 | var customer = result.Response; 76 | 77 | //Sparse update some properties 78 | result = await dataService.PostAsync(new Customer 79 | { 80 | Id = customer.Id, 81 | SyncToken = customer.SyncToken, 82 | GivenName = "Ross", 83 | sparse = true 84 | }); 85 | 86 | //Update all properties 87 | customer = result.Response; 88 | customer.FamilyName = "Geller"; 89 | customer.sparse = false; 90 | result = await dataService.PostAsync(customer); 91 | ``` 92 | 93 | ## Delete entities 94 | ```csharp 95 | var result = await dataService.PostAsync(new Invoice { Id = "123", SyncToken = syncToken }, OperationEnum.delete); 96 | ``` 97 | 98 | ## Querying entities 99 | ```csharp 100 | var result = await dataService.QueryAsync("SELECT * FROM Customer") 101 | //res.Response.Entities is of type Customer[] 102 | var customers = res.Response.Entities; 103 | ``` 104 | 105 | ```csharp 106 | var result = await dataService.QueryCountAsync("SELECT COUNT(*) FROM Customer"); 107 | var count = res.Response.TotalCount; 108 | ``` 109 | 110 | ## Querying reports 111 | ```csharp 112 | var report = await dataService.GetReportAsync("ProfitAndLoss", new() 113 | { 114 | { "accounting_method", "Accrual" }, 115 | { "date_macro", "Last Fiscal Year" } 116 | }); 117 | string reportName = report.Header.ReportName; 118 | ``` 119 | 120 | ## Change Data Capture (CDC) 121 | ```csharp 122 | var result = await dataService.GetCDCAsync(DateTimeOffset.UtcNow.AddDays(-10), "Customer,Invoice"); 123 | var queryResponses = result.Response.QueryResponse; //type QueryResponse[] 124 | var customers = queryResponses[0].IntuitObjects.Cast(); 125 | var invoices = queryResponses[1].IntuitObjects.Cast(); 126 | ``` 127 | 128 | ## Batch 129 | ```csharp 130 | //Delete 30 bills in a batch 131 | var bills = (await dataService.QueryAsync("SELECT * FROM Bill MAXRESULTS 30")).Response.Entities; 132 | var response = await dataService.BatchAsync(new IntuitBatchRequest 133 | { 134 | BatchItemRequest = bills.Select(b => new BatchItemRequest 135 | { 136 | bId = Guid.NewGuid().ToString(), 137 | operation = OperationEnum.delete, 138 | Bill = new Bill 139 | { 140 | Id = b.Id, 141 | SyncToken = b.SyncToken 142 | } 143 | }).ToArray() 144 | }); 145 | 146 | //Issue multiple queries in a batch 147 | var response = await dataService.BatchAsync(new IntuitBatchRequest 148 | { 149 | BatchItemRequest = new[] 150 | { 151 | new BatchItemRequest 152 | { 153 | bId = Guid.NewGuid().ToString(), 154 | Query = "SELECT * FROM Bill MAXRESULTS 30", 155 | }, 156 | new BatchItemRequest 157 | { 158 | bId = Guid.NewGuid().ToString(), 159 | Query = "SELECT * FROM Invoice MAXRESULTS 30", 160 | } 161 | } 162 | }); 163 | ``` 164 | 165 | ## Verifying webhooks 166 | ```csharp 167 | [HttpPost] 168 | [IgnoreAntiforgeryToken] 169 | [AllowAnonymous] 170 | public async Task Webhook() 171 | { 172 | string signature = Request.Headers["intuit-signature"].ToString(); 173 | string webhookVerifierToken = //get from config 174 | string requestBodyJSON = await base.ReadBodyToEndAsync(); 175 | if (!Helper.IsAuthenticWebhook(signature, webhookVerifierToken, requestBodyJSON)) 176 | return BadRequest(); 177 | //return HTTP error status 178 | 179 | //Process webhook 180 | WebhookEvent notification = JsonSerializer.Deserialize(requestBodyJSON, QuickBooksHttpClient.JsonSerializerOptions); 181 | } 182 | ``` 183 | 184 | ## Download Invoice PDF 185 | ```csharp 186 | var invoiceId = "1023"; 187 | var invoidePdfStream = await dataService.GetInvoicePDF(invoiceId); 188 | ``` 189 | --------------------------------------------------------------------------------